13. Двоичная программа на Си#
Введение#
Под простой программой будем понимать программу, полученную из одного файла исходного кода.
В UNIX можно выделить два типа программ:
двоичную программу с инструкциями для ЦП;
текстовую программу с инструкциями для интерпретатора.
Текстовые программы называют сценариями или скриптами.
Их пошагово выполняет интерпретатор, который можно рассматривать как “виртуальный” процессор.
Им может быть не только командная оболочка bash, но и интерпретаторы языков программирования python, perl.
Двоичную программу исполняет ЦП. Она состоит из машинных инструкций и начальных значений у глобальных данных и константных данных, необходимых при исполнении. Такие программы получают преобразованием текстовых файлов, написанных на языке программирования, утилитами компилятора.
Из описаний выше двоичных и текстовых программ вытекают следующие различия в:
переносимости;
скорости исполнения;
сложности с модификацией.
Под переносимостью программы подразумевают возможность ее запуска на другом компьютере. Двоичную программу можно запустить на компьютере с той же платформой (комбинацией типа ОС и архитектуры ЦП), под которую она была подготовлена. Запуск на отличающейся платформе потребует повторной компиляции программ под нее. Текстовая программа же запустится на любом компьютере с установленным интерпретатором. В обоих случаях необходима подготовка: в первом подготавливается исполняемый файл и этим занимается разработчик, а во втором настраивается системное окружение (системный администратор устанавливает интерпретатор).
Двоичные программы от текстовых отличаются высокой скоростью исполнения. Интерпретатор на каждом запуске разбирает текст программы и отражает его в машинном коде. При использовании компилятора эти действия происходят один раз при получении исполняемого файла.
В отличие от двоичных программ, текстовую программу не надо компилировать и можно запустить сразу после редактирования. Для внесения изменений достаточно только текстового редактора. Поэтому сценарии применяют там, где требуется автоматизация рутинных действий.
Создание программы#
Файлы с двоичными программами можно поделить на две части – это запускаемые файлы и разделяемые библиотеки.
Разделяемые библиотеки (динамические библиотеки в Windows) представляют повторно используемые части программ.
Они пристыковываются системным загрузчиком к программе при запуске.
У запускаемых файлов расширение как правило отсутствует, а у разделяемых библиотек – это .so (сокращение от “shared object”).
Исполняемые файлы являются результатом преобразования текстовых файлов. Такое преобразование часто называют компиляцией. Если быть точным, то компиляция – это только один из этапов преобразования наравне с предобработкой, компоновкой и оптимизацией. Поэтому преобразование текстовых файлов в исполняемый лучше называть сборкой программы.
Текстовые файлы оформлены по правилам языка программирования.
Содержимое файлов еще называют исходным кодом.
В зависимости от того, на каком языке написан исходный код, файл имеет одно из расширений.
Для языка программирования Си используют расширение .c, а для C++ – .cpp (также распространены .cxx, .cc).
Сборку программ в Linux осуществляют компиляторы, поставляемые проектом “GNU GCC”.
Для Windows эти же компиляторы доступны через проекты Cygwin и MinGW.
Набор утилит для программирования поставляет пакет build-essential.
Чтобы проверить доступность компиляторов на машине, запустите команду gcc --version:
bob@pc:~$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
В проект “GNU GCC” входят компиляторы языков программирования Си, C++, Objective-C, Fortran, Go, Ada и другие.
Список доступных компиляторов можно узнать из вывода команды gcc -v в записи --enable-languages.
Проект GNU GCC предлагает множество утилит командной строки.
Среди них всех представляет интерес управляющая утилита gcc.
Чтобы собрать простую программу достаточно gcc передать имя файла: gcc main.c
На выходе появится исполняемый файл a.out.
Имя выходного файла можно переопределить после опции -o: gcc main.c -o program.
Познакомимся с некоторыми опциями.
Опция -g добавляет отладочные символы в программу.
Отладочные символы увеличивают размер выходного файла, но позволяет увидеть в отладчике значения переменных.
Также будет полезна опция -Wall.
Она разрешит компилятору выводить все предупреждения на потенциально опасные участки кода.
Вывод переменных окружения#
Главная функция main программы имеет несколько форм, из которых нам знакомы две:
int main()int main(int argc, char** argv)
Также доступна и другая форма функции, в которой третьим аргументом вводится массив с переменными окружения:
int main(int argc, char** argv, char** envp)
Массив envp состоит из указателей на строки.
Последний элемент в массиве отмечен нулевым символом.
Иллюстрация к массиву char** envp:
┌─────────────> SHELL=/bin/bash
│ ┌────────> LOGNAME=skt
│ │ ┌───> PWD=/home/skt/projects/sppo
┌──┼─┬──┼─┬──┼─┬──⋅⋅⋅┬ ───┐
envp ────>│ x │ x │ x │ │NULL│
└────┴────┴────┴──⋅⋅⋅┴────┘
Тот же самый массив доступен из любой точки программы из глобальной переменной environ, которую предварительно следует объявить как extern char **environ.
Напишем альтернативу команде printenv.
Она обходит массив envp и выводит на экран его элементы, т.е. переменные окружения.
#include <stdio.h>
int main(int argc, char** argv, char** envp)
{
char** ptr = envp;
while(*ptr != NULL)
{
printf("%s\n", *ptr);
++ptr;
}
return 0;
}
Чтение файла построчно#
Разберем небольшую программу на Си, выводящую на экран текстовый файл по строкам.
Программа оформлена вокруг функции getline(3) из заголовочного файла stdio.h.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv)
{
FILE* file = fopen("data.txt", "r");
if(!file)
return 1;
char* line = NULL;
size_t n = 0;
while(getline(&line, &n, file) != -1)
printf("%s", line);
free(line);
fclose(file);
return 0;
}
Функция помещает строку из файлового потока в буфер и имеет следующую форму:
ssize_t getline(char **lineptr, size_t *n, FILE *stream)
Файловый поток представлен параметром stream, а буфер двумя параметрами: указателем на строку lineptr и размером строки n.
Перед вызовом функции буфер может быть пуст (*lineptr == NULL и n = 0) или может содержать указатель на память из кучи и размер этой памяти.
Если память под буфер не выделена или она недостаточна для сохранения результата, то функция getline скорректирует его размер.
Функция возвращает количество прочитанных символов или -1 при достижении конца файла.
По завершению программы память под буфер должна быть возвращена системе вызовом функции free.
Для работы с файлом в Си используем структуру FILE и функции для открытия fopen(3) и закрытия fclose(3).
Функция FILE *fopen(const char *pathname, const char *mode) открывает файл и возвращает ассоциированный с ним указатель на файловый поток.
Первый параметр содержит строку с именем файла, а второй параметр – настройки для потока.
Строка "r" указывает, что открывается текстовый файл для чтения.
После завершения работы с файлом, он должен быть закрыт функцией fclose.
Использованные источники#
Вопросы для самоконтроля#
Какие типы программ вам известны?
Какие расширения дают исполняемым файлам?
Чем отличаются двоичные программы от текстовых?
Как превратить созданный в редакторе текстовый файл в исполняемый?
Какие существуют механизмы для запуска пользовательских программ?
Как указывают интерпретатор для сценария?
Упражнения#
Оформите сценарий bash, который формирует следующие вложенные каталоги и файлы:
project
├─> src
│ ├─> app ─> ∅
│ ├─> core ─> ∅
│ └─> utility ─> ∅
├─> test
│ ├─> gtest ─> ∅
│ └─> unittests ─> ∅
├─> build
│ ├─> build-windows.bat
│ └─> build-unix.sh
├─> cmake ─> ∅
└─> CMakeLists.txt
Модифицируйте программу вывода переменных окружения на экран так, чтобы она брала строки из глобальной переменной
char** environ.Напишите собственную упрощенную версию утилиты
echo, которая отправит в стандартный вывод переданные ей аргументы командной строки. Для отправки строки в стандартный вывод воспользуйтесь функциейfputs(3)и файловым потоком из глобальной переменнойstdout.Напишите собственную утилиту
pwd. Вам понадобится функция стандартной библиотекиstrcmp(3).Напишите собственную утилиту
cat.Напишите программу, которая ищет в списке аргументов командной строки опцию
--helpи выводит сообщение с краткой справкой.Напишите свой “виртуальный процессор”, который будет выполнять сценарии, просто отправляя на экран его строки. В
getline(3)показан пример исходного кода считывающий последовательно строки из файла.Найдите на GitHub исходные коды утилит
trueиfalse. Как разработчики смогли реализовать одно, используя программный код другого?Напишите программу, выполняющую длительную операцию. Прервите программу разными сигналами и посмотрите, какой код ошибки будет возвращен.