🪓 Гайд на Си

👋 Aloe! Давно хотел разобраться в Сишке, но не мог найти доку в современном представлении как для Rust. А оказывается надо было искать не доку а руководство от компилятора
0️⃣ Сборка 
gcc набор компиляторов для различных языков: Си, C++, Objective-C, Java, Go, D...
clang является фронтендом для языков программирования Си использующимся совместно с фреймворком LLVM. Целью проекта является создание замены gcc
На MacOS уже установлен clang
Не понятно почему clang имеет псевдоним gcc
clang/gcc/cc
$ clang -O2 -i input.c -o output -Wall -Wextra -Werror -pedantic- -O2уровень оптимизации кода
- -iнаш файл входа (можно без флага)
- -oназвание файла на выходе (по умолчанию вернется a.out)
- -Wallвывод всех предупреждений
- -Wextraне уверен что они входят в общий
- -Werrorпредупреждения как ошибки
- -pedanticпроверка по стандарту
Этапы компиляции
- file.iобработка директив препроцессора (вставка/раскрытие кода)
- file.sкомпиляция в ассемблер (низкоуровневый код)
- file.oобъектный файл для линкера
- исполняемый файл
--save-temps сохраняет промежуточные файлы
Хороший тон
- main.cдля файла с точкой входа
- graph_const.hпример заголовочника
- includeдериктория хранит заголовочники
project
├── include
│   ├── common.h
│   ├── network.h
│   └── packet.h
├── common.c
├── main.c
├── network.c
├── packet.c
└── MakefileИспользуйте Makefile для автоматизации
fname_input = main.c
fname_output = main.bin
CFLAGS = -O2 -Wall -Wextra -Werror -pedantic
AC_RED     = \x1b[31m
AC_GREEN   = \x1b[32m
AC_YELLOW  = \x1b[33m
AC_RESET   = \x1b[0m
default: dev
test: main_file = test.c
test: dev;
dev: compile run clean
compile:
	@echo "${AC_YELLOW}[${fname_input}]: Compiling...${AC_RESET}"
	@clang ${fname_input} -o ${fname_output} ${CFLAGS}
run:
	@echo "${AC_GREEN}[${fname_input}]: Running...${AC_RESET}\n"
	@./${fname_output}
clean:
	@echo "\n${AC_RED}[${fname_input}]: Cleaning...${AC_RESET}"
	@rm -f ${fname_output}У clang большой help🙈, надо юзать man:
$ man clanghexdump, порядок байтов, кодировки
Чтобы узнать как хранится файл используем hexdump
$ hexdump main.c | less
  0000000 6923 636e 756c 6564 3c20 7473 6964 2e6f
  0000010 3e68 230a 6e69 6c63 6475 2065 733c 6474Нужно учитывать порядок байтов процессора, для MacOS это little endian значит надо поменять соседние биты местами.
$ hexdump -C main.c | less
  00000000  23 69 6e 63 6c 75 64 65  20 3c 73 74 64 69 6f 2e  |#include <stdio.|
  00000010  68 3e 0a 23 69 6e 63 6c  75 64 65 20 3c 73 74 64  |h>.#include <std|Оптимальная таблица символов устанавливается автоматически
$ file main.c
  main.c: c program text, ASCII text
$ hexdump -C main.c | less
  ...
  00000060  54 20 22 41 42 43 44 45  46 47 48 49 4a 4b 4c 4d  |T "ABCDEFGHIJKLM|Если добавить кириллицу меняется стандарт/таблица символов ASCII на Unicode
UTF-8 это кодировка/формат
$ file main.c
  main.c: c program text, Unicode text, UTF-8 text
$ hexdump -C main.c | less
  ...
  00000060  54 20 22 d0 a4 41 42 43  44 45 46 47 48 49 4a 4b  |T "ФABCDEFGHIJK|В ASCII символ занимает всегда один байт, а в UTF-8 один или два байта.
Unicode включает первые 0x7F = 128 значений из ASCII, дальше идут секции других языков.
И как они вместе существуют?
Чтобы не зацепить ASCII символы во время decode/encode в любом порядке байтов:
- Секции начинаются от 0xC2
- Элементы секций начинаются с 0x80и заканчиваются0xBF
В примере видим что 0xD0 0xA4 является буквой Ф которую можно найти в секции 0xD0 на позиции 0xA4
При необходимости можно установать utf-8 bom
bom — byte order mark в начале файла (3 байта)
Чтобы хранить маркер для согласования порядка байт и кодировки
В vim можно настроить через set bomb/nobomb
$ file main.c
  main.c: c program text, Unicode text, UTF-8 (with BOM) text
$ hexdump -C main.c | less
  00000000  ef bb bf 23 69 6e 63 6c  75 64 65 20 3c 73 74 64  |#include <std|
  00000010  69 6f 2e 68 3e 0a 23 69  6e 63 6c 75 64 65 20 3c  |io.h>.#include <|1️⃣ Директивы препроцессора 
#include 
Подключает заголовочные файлы или стандартные либы
#include <stdio.h>
int main() {
  puts("aloe");
}#define 
Определяет макросы
Макросэто фрагмент кода, которому присвоено имя
Препроцессор подставит(продублирует) значение.
Название обычно в стиле SCREAMING_SNAKE_CASE
#include <stdio.h>
#define VERA "🥳"
#define SUM(a,b) (a + b)
#define ALOE() aloe()
int aloe() {
  return SUM(11, 22);
}
int main() {
  ALOE();
  printf("aloe %s\n", VERA);
}2️⃣ Типы данных 
Название обычно в стиле snake_case
Мусор в переменных
По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL. Если объявить и не задать значение.
Целочисленные 
Более информативные названия в stdint.h
- uint8_t- uint16_t- uint32_t- uint64_t
- int8_t- int16_t- int32_t- int64_t
Ограничения можно узнать в limits.h
8-bit
| Название | Описание | 
|---|---|
| char | для ASCII | 
| signed char | int8 | 
| unsigned char | uint8 | 
16-bit
| Название | Описание | 
|---|---|
| short int | int16 | 
| unsigned short int | uint16 | 
32-bit
| Название | Описание | 
|---|---|
| int | int32 | 
| unsigned int | uint32 | 
64-bit
| Название | Описание | 
|---|---|
| long long int | int64 | 
| unsigned long long int | uint64 | 
Если начинается:
- 0xшестнадцатеричное
- 0восьмеричное
char symbol = 'a';
unsigned char hex = 0xff;
int a = -1, b = -2, c = -3;Если заканчивается:
- uбеззнаковый целочисленный тип
- lдлинный целочисленный тип
Буквы можно использовать в любом порядке.
45u; // unsigned int
45ul; // unsigned long int
45ull; // unsigned long long intДробные 
Ограничения можно узнать в float.h
| Название | Описание | 
|---|---|
| float | FLT_MIN..FLT_MAX: 1e-37..1e37 | 
| double | >= float | 
| long double | >= double | 
float aloe = 0.12;
double vera = 114.0;Символы 
LF = EOF = \0 - cимвол конца строки
| Название | Описание | 
|---|---|
| \a | звуковой сигнал | 
| \b | backspace | 
| \n | переход на новую строку | 
| \r | возврат коретки | 
| \t | горизонтальный таб | 
| \v | вертикальный таб | 
char a = 'A';
char a_oct = '\101'; // восьмеричное A
char a_hex = '\x41'; // шестнадцатеричное AМассивы/Множества 
По факту мы создаем указатель на первый элемент
int arr[] = { 1, 2, 3 };
*arr;       // arr[0]
*(arr + 1); // arr[1]
arr[2];
arr[3];     // чужой огород
int bb[4];  // хранит мусорАвтозаполнение 
Можно задать элементы, а остальные примут значение по умолчанию
int a[3] = { 2, 1 }; // [2, 1, 0]
char b[3] = { 'a' }; // ['a', '\0', '\0']Строки 
Если массив символов заканчивается символом конца строки \0
Всю строку заменить нельзя, но можно заменить символы
char empty[10] = { 0 };         // пустая строка
char ab[] = { 'a', 'b', '\0'};
char abe[] = "abe";             // 3 символа + конец строки
char aloe[10] = "aloe";         // 10 символов + конец строки
char text[] = "Today's special is a pastrami sandwich on rye bread with \
a potato knish and a cherry soda."Конкатенация строк 🙊
Соседние строковые константы объединяются в одну строку, при этом в конец последней объединенной строки добавляется нулевой символ завершения
"tutti frutti ice cream"
// можно через пробел или перенос строки
"tutti " "frutti"
" ice " "cream"Константы 
Доступ только для чтения
const int b  = 123;
void aloe(const int);Способ хранения 
По умолчанию auto. Являются локальными, существуют в объявленной области
{
  auto int a = 1; 
  int b = 2;
}static значения будут записаны в блок данных программы, существуют в файле
int counter() {
  static int counter = 0; // вне стека фрейма функции
  return ++counter;
}extern экспорт из файла. Нельзя сразу инициализировать
extern int numberOfClients;
int numberOfClients = 0;Замудренные 🥴
register дает компилятору указание по возможности хранить переменную в регистрах процессора, а не в оперативной памяти. При объявлении переменной-итератора цикла с небольшим телом может повысить скорость работы всего цикла в несколько раз.
register byte i = 0;
for (i; i < 256; i++)
    check_value(i);restrict при объявлении указателя дает компилятору гарантию (вы, как программист, гарантируете), что ни один указатель не будет указывать на область памяти, на которую указывает целевой указатель. Профит этого модификатора в том, что компилятору не придется проверять, не указывает ли какой-то еще указатель на целевой блок памяти. Если у вас внутри функции несколько указателей одного типа — возможно, он вам пригодится.
void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val);volatile указывает компилятору, что переменная может быть изменена неявным для него образом. Даже если компилятор пометит код, зависимый от волатильной переменной, как dead code (никогда не будет выполнен), он не будет выброшен, и в рантайме выполнится в полном объеме.
int var = 1;
if (!var)             /* Эти 2 строчки будут отброшены компилятором */
    dosmthng();   
volatile int var = 1;
if (!var)            /* А вот эти  - нет */
    dosmthng();Псевдонимы 
typedef unsigned char byte_type;
typedef double real_number_type;
typedef char array_of_bytes [5]; 
array_of_bytes Five_bytes = {0, 1, 2, 3, 4};Выбор имени типов
Не следует заканчивать имена типов на _t из-за системных типов например
Приведение типов 
(double)10 / 3;4️⃣ Функции 
Аргументы 
void main(int argc, char const* argv[]) {
  printf("argc => %i\n", argc);
  printf("argv[0] => %s\n", argv[0]);
}Явный отказ от параметров
Это было актуально до с99
void aloe(void);В теле функции параметр представляет собой локальную копию значения, переданного в функцию. Вы не можете изменить переданное значение, изменив локальную копию
Массив как параметр
sizeof вернет размер указателя, потому что массив передаётся как указатель
Прототипы 
Название переменных не обязательно
/* 
 * Название
 *
 * Описание
 *
 * @param int a - первый
 * @param int b - второй
 * @param int - третий
 * @return int
 */
int aloe(int a, char[], int);Хороший тон 👌
/* пример функции общего пользования */
static void dgtprint(char *str);
/* пример функции для работы со специфичным контекстом */
void enable_all_vlans(struct vlan_cfg *vp);Статические 
Вы можете определить функцию как статическую, если хотите, чтобы ее можно было вызывать только в исходном файле
static int foo(int x) {
  return x + 42;
}5️⃣ Форматированный вывод 
- %cсимвол
- %sстрока
- %uбеззнаковые целые числа
- %i- %dзнаковые целые числа
- %x- %X16-ричный формат
- lfдробные
- %zuразмер в памяти в байтах
printf("%c\n", 'u');
printf("%s\n", "aloe");
printf("%u\n", 123);
printf("%.2lf\n", 10.0 / 3);
printf("int size: %zu byte", sizeof(int));Заполнители
- Символ префикса
- Общее количество символов
// 0 - заполнитель, 4 - количесво
printf("%04i\n", -10 / 3);
printf("%08X\n", 123);6️⃣ Указатели 
Указатель обычная переменная со своим адресом, но хранит адрес другой переменной.
char num1 = 111;
char* num_ptr = &num1; // получить адресс переменной
char num2 = *num_ptr;  // получить значение по адресу (разыменование/deref)
*num_ptr = 333;        // изменить значение по адресуНе надо сохранять адресс в значение по адресу указателя.
int i, j;
int* ip = &i;  // ‘ip’ хранит адресс ‘i’
ip = &j;       // ‘ip’ хранит адресс ‘j’
*ip = &i;      // ‘j’  хранит адресс ‘i’Строки объявленные через указатель нельзя изменять.
char* aloe = "aloe vera";
aloe[4] = '-';Константы 
int a = 123, b = 321;
const int* aloe = &a; // нельзя менять значение
*aloe = 321;
aloe = &asd;int a = 123, b = 321;
int* const aloe = &a; // нельзя менять адресс
*aloe = 321;
aloe = &asd;Массив указателей - указатель на указатель 
char* arr[] = { "aloe", "vera" }
char arr[1][3] = 'k';
void aloe(const char** arr)Указатель на функцию 
#include <stdio.h>
void foo (int i) {
  printf ("foo %d!\n", i);
}
void bar (int i) {
  printf ("%d bar!\n", i);
}
void message (void (*func)(int), int times) {
  for (int j = 0;j < times; j++) func(j); // (*func)(j)
}
void example (int want_foo) {
  void (*pf)(int) = want_foo ? foo : &bar; // & не обязательно
  message(pf, 5);
}7️⃣ Шифратор и дешифратор шифра цезаря 
- Проверка аргументов
- Шифратор и дешифратор
$ make -- dev --brute-force SVVRZSPRLFVBOHJRLKTL
$ make -- dev --offset 7 LOOKSLIKEYOUHACKEDME
$ make -- dev --decode --offset 7 SVVRZSPRLFVBOHJRLKTL#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define ALPHABET "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define ALPHABET_SIZE (int)strlen(ALPHABET)
#define AC_RED     "\x1b[31m"
#define AC_GREEN   "\x1b[32m"
#define AC_YELLOW  "\x1b[33m"
#define AC_BLUE    "\x1b[34m"
#define AC_MAGENTA "\x1b[35m"
#define AC_CYAN    "\x1b[36m"
#define AC_RESET   "\x1b[0m"
void print_alphabet(const char alphabet[])
{
  for (int i = 0; i < ALPHABET_SIZE; i++) {
    printf(" %c ", alphabet[i]);
  }
  puts("");
}
char* get_alphabet_with_offset(const int offset)
{
  static char alphabet[ALPHABET_SIZE] = { '\0' };
  for (unsigned int i = 0, j = 0; i < ALPHABET_SIZE; i++) {
    j = (i + offset) % ALPHABET_SIZE;
    alphabet[i] = ALPHABET[j];
  }
  // print_alphabet(alphabet);
  return alphabet;
}
int get_char_index(char input_char, const char alphabet[])
{
  for (int i = 0; i < ALPHABET_SIZE; i++) {
    if (alphabet[i] == input_char) {
      return i;
    }
  }
  return -1;
}
void decode(const char input[], const int offset)
{
  puts(AC_BLUE "Decode" AC_RESET);
  const char* offset_alphabet = get_alphabet_with_offset(offset);
  printf("[%02i]: ", offset);
  int length = strlen(input);
  for (int i = 0, j = 0; i < length; i++) {
    j = get_char_index(input[i], offset_alphabet);
    printf("%c", ALPHABET[j]);
  }
  puts("");
}
void encode(const char input[], const int offset)
{
  puts(AC_MAGENTA "Encode" AC_RESET);
  const char* offset_alphabet = get_alphabet_with_offset(offset);
  printf("[%02i]: ", offset);
  int length = strlen(input);
  for (int i = 0, j = 0; i < length; i++) {
    j = get_char_index(input[i], ALPHABET);
    printf("%c", offset_alphabet[j]);
  }
  puts("");
}
void brute_force(const char input[]) {
  for (int i = 1; i < ALPHABET_SIZE; i++) {
    decode(input, i);
  }
}
const char * const flags[] = { "--decode", "--brute-force" ,"--encode", "--offset" };
const int flags_length = sizeof flags / sizeof flags[0];
// void(*commands[])(const char input[], const int offset) = { decode, encode };
void validate_args(const int argc, const char* const argv[])
{
  if (argc == 1) {
    printf(AC_RED "[-] Error: Empty flags%s\n", AC_RESET);
    exit(EXIT_FAILURE);
  }
  bool has_decode = false;
  bool has_brute_force = false;
  bool has_encode = false;
  int offset = 0;
  const char* input = NULL;
  char is_valid = 0;
  for (int i = 1; i < argc; i++) {
    is_valid = 0;
    for (int j = 0; j < flags_length; j++) {
      if (strcmp(argv[i], flags[j]) == 0) {
        is_valid = 1;
        if (j == 0) {
          has_decode = true;
        } else if (j == 1) {
          has_brute_force = true;
        } else if (j == 2) {
          has_encode = true;
        } else if (j == 3) {
          offset = atoi(argv[i + 1]) % ALPHABET_SIZE;
        }
        break;
      }
    }
    if (argv[i][0] != '-') {
      input = argv[i];
    } else if (!is_valid) {
      printf(AC_RED "[-] Error: Incorrect flag was given! [%s]%s\n", argv[i], AC_RESET);
      exit(EXIT_FAILURE);
    }
  }
  if (input == NULL) {
    printf(AC_RED "[-] Error: Empty input string%s\n", AC_RESET);
    exit(EXIT_FAILURE);
  }
  if (has_decode) decode(input, offset);
  if (has_brute_force) brute_force(input);
  if (has_encode) encode(input, offset);
}
int main(const int argc, const char* const argv[])
{
  validate_args(argc, argv);
  // return EXIT_SUCCESS;
}