Skip to content

🪓 Гайд на Си

Preview

👋 Aloe! Давно хотел разобраться в Сишке, но не мог найти доку в современном представлении как для Rust. А оказывается надо было искать не доку а руководство от компилятора

0️⃣ Сборка

gcc набор компиляторов для различных языков: Си, C++, Objective-C, Java, Go, D...

clang является фронтендом для языков программирования Си использующимся совместно с фреймворком LLVM. Целью проекта является создание замены gcc

На MacOS уже установлен clang

Не понятно почему clang имеет псевдоним gcc

clang/gcc/cc

bash
$ clang -O2 -i input.c -o output -Wall -Wextra -Werror -pedantic
  • -O2 уровень оптимизации кода
  • -i наш файл входа (можно без флага)
  • -o название файла на выходе (по умолчанию вернется a.out)
  • -Wall вывод всех предупреждений
  • -Wextra не уверен что они входят в общий
  • -Werror предупреждения как ошибки
  • -pedantic проверка по стандарту
Этапы компиляции
  1. file.i обработка директив препроцессора (вставка/раскрытие кода)
  2. file.s компиляция в ассемблер (низкоуровневый код)
  3. file.o объектный файл для линкера
  4. исполняемый файл

--save-temps сохраняет промежуточные файлы

Хороший тон

  • main.c для файла с точкой входа
  • graph_const.h пример заголовочника
  • include дериктория хранит заголовочники
md
project
├── include
│   ├── common.h
│   ├── network.h
│   └── packet.h
├── common.c
├── main.c
├── network.c
├── packet.c
└── Makefile
Используйте 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:

bash
$ man clang
hexdump, порядок байтов, кодировки

Чтобы узнать как хранится файл используем hexdump

bash
$ 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 значит надо поменять соседние биты местами.

bash
$ 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|

Оптимальная таблица символов устанавливается автоматически

bash
$ 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 это кодировка/формат

bash
$ 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 в любом порядке байтов:

  1. Секции начинаются от 0xC2
  2. Элементы секций начинаются с 0x80 и заканчиваются 0xBF

В примере видим что 0xD0 0xA4 является буквой Ф которую можно найти в секции 0xD0 на позиции 0xA4

При необходимости можно установать utf-8 bom

bombyte order mark в начале файла (3 байта)

Чтобы хранить маркер для согласования порядка байт и кодировки

В vim можно настроить через set bomb/nobomb

bash
$ 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

Подключает заголовочные файлы или стандартные либы

c
#include <stdio.h>

int main() {
  puts("aloe");
}

#define

Определяет макросы

Макрос это фрагмент кода, которому присвоено имя

Препроцессор подставит(продублирует) значение.

Название обычно в стиле SCREAMING_SNAKE_CASE

c
#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 charint8
unsigned charuint8

16-bit

НазваниеОписание
short intint16
unsigned short intuint16

32-bit

НазваниеОписание
intint32
unsigned intuint32

64-bit

НазваниеОписание
long long intint64
unsigned long long intuint64

Если начинается:

  • 0x шестнадцатеричное
  • 0 восьмеричное
c
char symbol = 'a';
unsigned char hex = 0xff;
int a = -1, b = -2, c = -3;

Если заканчивается:

  • u беззнаковый целочисленный тип
  • l длинный целочисленный тип

Буквы можно использовать в любом порядке.

c
45u; // unsigned int
45ul; // unsigned long int
45ull; // unsigned long long int

Дробные

Ограничения можно узнать в float.h

НазваниеОписание
floatFLT_MIN..FLT_MAX: 1e-37..1e37
double>= float
long double>= double
c
float aloe = 0.12;
double vera = 114.0;

Символы

LF = EOF = \0 - cимвол конца строки

НазваниеОписание
\aзвуковой сигнал
\bbackspace
\nпереход на новую строку
\rвозврат коретки
\tгоризонтальный таб
\vвертикальный таб
c
char a = 'A';
char a_oct = '\101'; // восьмеричное A
char a_hex = '\x41'; // шестнадцатеричное A

Массивы/Множества

По факту мы создаем указатель на первый элемент

c
int arr[] = { 1, 2, 3 };
*arr;       // arr[0]
*(arr + 1); // arr[1]
arr[2];
arr[3];     // чужой огород
int bb[4];  // хранит мусор

Автозаполнение

Можно задать элементы, а остальные примут значение по умолчанию

c
int a[3] = { 2, 1 }; // [2, 1, 0]
char b[3] = { 'a' }; // ['a', '\0', '\0']

Строки

Если массив символов заканчивается символом конца строки \0

Всю строку заменить нельзя, но можно заменить символы

c
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."

Конкатенация строк 🙊

Соседние строковые константы объединяются в одну строку, при этом в конец последней объединенной строки добавляется нулевой символ завершения

c
"tutti frutti ice cream"
// можно через пробел или перенос строки
"tutti " "frutti"
" ice " "cream"

Константы

Доступ только для чтения

c
const int b  = 123;
void aloe(const int);

Способ хранения

По умолчанию auto. Являются локальными, существуют в объявленной области

c
{
  auto int a = 1; 
  int b = 2;
}

static значения будут записаны в блок данных программы, существуют в файле

c
int counter() {
  static int counter = 0; // вне стека фрейма функции
  return ++counter;
}

extern экспорт из файла. Нельзя сразу инициализировать

c
extern int numberOfClients;

int numberOfClients = 0;
Замудренные 🥴

register дает компилятору указание по возможности хранить переменную в регистрах процессора, а не в оперативной памяти. При объявлении переменной-итератора цикла с небольшим телом может повысить скорость работы всего цикла в несколько раз.

c
register byte i = 0;
for (i; i < 256; i++)
    check_value(i);

restrict при объявлении указателя дает компилятору гарантию (вы, как программист, гарантируете), что ни один указатель не будет указывать на область памяти, на которую указывает целевой указатель. Профит этого модификатора в том, что компилятору не придется проверять, не указывает ли какой-то еще указатель на целевой блок памяти. Если у вас внутри функции несколько указателей одного типа — возможно, он вам пригодится.

c
void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val);

volatile указывает компилятору, что переменная может быть изменена неявным для него образом. Даже если компилятор пометит код, зависимый от волатильной переменной, как dead code (никогда не будет выполнен), он не будет выброшен, и в рантайме выполнится в полном объеме.

c
int var = 1;
if (!var)             /* Эти 2 строчки будут отброшены компилятором */
    dosmthng();   

volatile int var = 1;
if (!var)            /* А вот эти  - нет */
    dosmthng();

Псевдонимы

c
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 из-за системных типов например

Приведение типов

c
(double)10 / 3;

4️⃣ Функции

Аргументы

c
void main(int argc, char const* argv[]) {
  printf("argc => %i\n", argc);
  printf("argv[0] => %s\n", argv[0]);
}

Явный отказ от параметров

Это было актуально до с99

c
void aloe(void);

В теле функции параметр представляет собой локальную копию значения, переданного в функцию. Вы не можете изменить переданное значение, изменив локальную копию

Массив как параметр

sizeof вернет размер указателя, потому что массив передаётся как указатель

Прототипы

Название переменных не обязательно

c
/* 
 * Название
 *
 * Описание
 *
 * @param int a - первый
 * @param int b - второй
 * @param int - третий
 * @return int
 */
int aloe(int a, char[], int);

Хороший тон 👌

c
/* пример функции общего пользования */
static void dgtprint(char *str);
/* пример функции для работы со специфичным контекстом */
void enable_all_vlans(struct vlan_cfg *vp);

Статические

Вы можете определить функцию как статическую, если хотите, чтобы ее можно было вызывать только в исходном файле

c
static int foo(int x) {
  return x + 42;
}

5️⃣ Форматированный вывод

  • %c символ
  • %s строка
  • %u беззнаковые целые числа
  • %i %d знаковые целые числа
  • %x %X 16-ричный формат
  • lf дробные
  • %zu размер в памяти в байтах
c
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));

Заполнители

  1. Символ префикса
  2. Общее количество символов
c
// 0 - заполнитель, 4 - количесво
printf("%04i\n", -10 / 3);
printf("%08X\n", 123);

6️⃣ Указатели

Указатель обычная переменная со своим адресом, но хранит адрес другой переменной.

c
char num1 = 111;
char* num_ptr = &num1; // получить адресс переменной
char num2 = *num_ptr;  // получить значение по адресу (разыменование/deref)
*num_ptr = 333;        // изменить значение по адресу

Не надо сохранять адресс в значение по адресу указателя.

c
int i, j;
int* ip = &i;  // ‘ip’ хранит адресс ‘i’
ip = &j;       // ‘ip’ хранит адресс ‘j’
*ip = &i;      // ‘j’  хранит адресс ‘i’

Строки объявленные через указатель нельзя изменять.

c
char* aloe = "aloe vera";
aloe[4] = '-';

Константы

c
int a = 123, b = 321;
const int* aloe = &a; // нельзя менять значение
*aloe = 321;
aloe = &asd;
c
int a = 123, b = 321;
int* const aloe = &a; // нельзя менять адресс
*aloe = 321;
aloe = &asd;

Массив указателей - указатель на указатель

c
char* arr[] = { "aloe", "vera" }
char arr[1][3] = 'k';

void aloe(const char** arr)

Указатель на функцию

c
#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️⃣ Шифратор и дешифратор шифра цезаря

  1. Проверка аргументов
  2. Шифратор и дешифратор
bash
$ make -- dev --brute-force SVVRZSPRLFVBOHJRLKTL
$ make -- dev --offset 7 LOOKSLIKEYOUHACKEDME
$ make -- dev --decode --offset 7 SVVRZSPRLFVBOHJRLKTL
c
#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;
}