🪓 Гайд на Си
👋 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 clang
hexdump, порядок байтов, кодировки
Чтобы узнать как хранится файл используем 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
%X
16-ричный формат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;
}