# Двоичная программа на Си


## Введение

Под простой программой будем понимать программу, полученную из одного файла исходного кода.

В UNIX можно выделить два типа программ:
* *двоичную программу* с инструкциями для ЦП;
* *текстовую программу* с инструкциями для интерпретатора.

<!-- Сценарии и интерпретаторы -->
Текстовые программы называют *сценариями* или *скриптами*.
Их пошагово выполняет интерпретатор, который можно рассматривать как "виртуальный" процессор.
Им может быть не только командная оболочка `bash`, но и интерпретаторы языков программирования `python`, `perl`.

<!-- Двоичные программы -->
Двоичную программу исполняет ЦП.
Она состоит из машинных инструкций и начальных значений у глобальных данных и константных данных, необходимых при исполнении.
Такие программы получают преобразованием текстовых файлов, написанных на языке программирования, утилитами компилятора.

Из описаний выше двоичных и текстовых программ вытекают следующие различия в:
* переносимости;
* скорости исполнения;
* сложности с модификацией.

<!-- Переносимость программ -->
Под переносимостью программы подразумевают возможность ее запуска на другом компьютере.
Двоичную программу можно запустить на компьютере с той же платформой (комбинацией типа ОС и архитектуры ЦП), под которую она была подготовлена.
Запуск на отличающейся платформе потребует повторной компиляции программ под нее.
Текстовая программа же запустится на любом компьютере с установленным интерпретатором.
В обоих случаях необходима подготовка: в первом подготавливается исполняемый файл и этим занимается разработчик, а во втором настраивается системное окружение (системный администратор устанавливает интерпретатор).

<!-- Скорость исполнения -->
Двоичные программы от текстовых отличаются высокой скоростью исполнения.
Интерпретатор на каждом запуске разбирает текст программы и отражает его в машинном коде.
При использовании компилятора эти действия происходят один раз при получении исполняемого файла.

<!-- Редактирование программы -->
В отличие от двоичных программ, текстовую программу не надо компилировать и можно запустить сразу после редактирования.
Для внесения изменений достаточно только текстового редактора.
Поэтому сценарии применяют там, где требуется автоматизация рутинных действий.


## Создание программы

<!-- Расширения исполняемых файлов -->
Файлы с двоичными программами можно поделить на две части -- это запускаемые файлы и разделяемые библиотеки.
Разделяемые библиотеки (динамические библиотеки в Windows) представляют повторно используемые части программ.
Они пристыковываются системным загрузчиком к программе при запуске.
У запускаемых файлов расширение как правило отсутствует, а у разделяемых библиотек -- это `.so` (сокращение от "shared object").

<!-- Сборка программы -->
Исполняемые файлы являются результатом преобразования текстовых файлов.
Такое преобразование часто называют компиляцией.
Если быть точным, то компиляция -- это только один из этапов преобразования наравне с предобработкой, компоновкой и оптимизацией.
Поэтому преобразование текстовых файлов в исполняемый лучше называть *сборкой программы*.

<!-- Расширение файлов с исходными кодами -->
Текстовые файлы оформлены по правилам языка программирования.
Содержимое файлов еще называют *исходным кодом*.
В зависимости от того, на каком языке написан исходный код, файл имеет одно из расширений.
Для языка программирования Си используют расширение `.c`, а для C++ -- `.cpp` (также распространены `.cxx`, `.cc`).

<!-- GNU GCC -->
Сборку программ в Linux осуществляют компиляторы, поставляемые проектом "GNU GCC".
Для Windows эти же компиляторы доступны через проекты Cygwin и MinGW.
Набор утилит для программирования поставляет пакет `build-essential`.
Чтобы проверить доступность компиляторов на машине, запустите команду `gcc --version`:
```console
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`.

<!-- Управляющая утилита `gcc` -->
Проект GNU GCC предлагает множество утилит командной строки.
Среди них всех представляет интерес управляющая утилита `gcc`.
Чтобы собрать простую программу достаточно `gcc` передать имя файла: `gcc main.c`
На выходе появится исполняемый файл `a.out`.
Имя выходного файла можно переопределить после опции `-o`: `gcc main.c -o program`.

<!-- Опции `-g` и `-Wall` -->
Познакомимся с некоторыми опциями.
Опция `-g` добавляет отладочные символы в программу.
Отладочные символы увеличивают размер выходного файла, но позволяет увидеть в отладчике значения переменных.
Также будет полезна опция `-Wall`.
Она разрешит компилятору выводить все предупреждения на потенциально опасные участки кода.


## Вывод переменных окружения

Главная функция `main` программы имеет несколько форм, из которых нам знакомы две:
* `int main()`
* `int main(int argc, char** argv)`

Также доступна и другая форма функции, в которой третьим аргументом вводится массив с переменными окружения:
```c++
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` и выводит на экран его элементы, т.е. переменные окружения.

```c++
#include <stdio.h>
int main(int argc, char** argv, char** envp)
{
  char** ptr = envp;
  while(*ptr != NULL)
  {
    printf("%s\n", *ptr);
    ++ptr;
  }
  return 0;
}
```

<!--
Стандартная библиотека Си предоставляет функции для работы с переменными окружения.
Функции объявлены в заголовочном файле `stdlib.h`:
* `char* getenv(const char* name)` возвращает указатель на строку со значением переменной, заданной именем `name`;
* `int setenv(const char* name, const char* value, int overwrite)` добавляет новую переменную или изменяет существующую, если аргумент `overwrite` имеет ненулевое значение;
* `int putenv(char* string)` добавляет или изменяет существующую переменную, имя и значение которой заданы в `string`;
* `int unsetenv(const char* name)` удаляет переменную с именем `name` из списка.
* `int clearenv(void)` удаляет все переменные из списка.
-->


## Чтение файла построчно

Разберем небольшую программу на Си, выводящую на экран текстовый файл по строкам.
Программа оформлена вокруг функции `getline(3)` из заголовочного файла `stdio.h`.

```c
#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;
}
```

<!-- Про функцию getline -->
Функция помещает строку из файлового потока в буфер и имеет следующую форму:
```c
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`.


## Использованные источники

1. [GCC and Make -- A Tutorial on how to compile, link and build C/C++ application](https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html)
2. getline(3)


## Вопросы для самоконтроля

1. Какие типы программ вам известны?
1. Какие расширения дают исполняемым файлам?
1. Чем отличаются двоичные программы от текстовых?
1. Как превратить созданный в редакторе текстовый файл в исполняемый?
1. Какие существуют механизмы для запуска пользовательских программ?
1. Как указывают интерпретатор для сценария?


## Упражнения

1. Оформите сценарий bash, который формирует следующие вложенные каталоги и файлы:
```
project
  ├─> src
  │    ├─> app ─> ∅
  │    ├─> core ─> ∅
  │    └─> utility ─> ∅
  ├─> test
  │    ├─> gtest ─> ∅
  │    └─> unittests ─> ∅
  ├─> build
  │    ├─> build-windows.bat
  │    └─> build-unix.sh
  ├─> cmake ─> ∅
  └─> CMakeLists.txt
```
2. Модифицируйте программу вывода переменных окружения на экран так, чтобы она брала строки из глобальной переменной `char** environ`.
3. Напишите собственную упрощенную версию утилиты `echo`, которая отправит в стандартный вывод переданные ей аргументы командной строки.
   Для отправки строки в стандартный вывод воспользуйтесь функцией `fputs(3)` и файловым потоком из глобальной переменной `stdout`.
4. Напишите собственную утилиту `pwd`.
   Вам понадобится функция стандартной библиотеки `strcmp(3)`.
5. Напишите собственную утилиту `cat`.
6. Напишите программу, которая ищет в списке аргументов командной строки опцию `--help` и выводит сообщение с краткой справкой.
7. Напишите свой "виртуальный процессор", который будет выполнять сценарии, просто отправляя на экран его строки.
   В `getline(3)` показан пример исходного кода считывающий последовательно строки из файла.
8. Найдите на GitHub исходные коды утилит `true` и `false`.
   Как разработчики смогли реализовать одно, используя программный код другого?
9. Напишите программу, выполняющую длительную операцию.
   Прервите программу разными сигналами и посмотрите, какой код ошибки будет возвращен.
