С/С++ Portal | Программирование
15.4K subscribers
1.3K photos
220 videos
26 files
901 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для C/C++-разработчика

Сотрудничество, реклама: @devmangx

Менеджер: @Spiral_Yuri

РКН: https://clck.ru/3Foc4d
Download Telegram
При программировании на C с компилятором GCC можно объявлять функции внутри функций.

Насколько известно, это работает только в GCC.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👀20🏆52💊1
В Linux есть фича уровня ядра, которая позволяет выполнять код на каждом сетевом пакете. Её используют Nginx и файрволы. Называется BPF.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13👀41
goto в C во многих туториалах подаётся как антипаттерн.
В ядре Linux kernel он активно используется для обработки ошибок.

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

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21👍73💊1
xor-двусвязный список — одна из тех вещей, которые выглядят слишком умно на первый взгляд

вместо того чтобы хранить prev и next, ты хранишь prev ^ next, так что да, экономишь один указатель на узел, но теперь при движении вперёд тебе каждый раз нужен предыдущий узел — напрямую перейти к следующему уже нельзя

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
14👀7🤔5
malloc не выделяет физическую память.

Он резервирует виртуальное адресное пространство. В Linux (overcommit) ядро возвращает успех даже при нехватке RAM. Реальное выделение происходит при первом доступе к странице (page fault).

Поэтому падения по OOM возникают далеко от места вызова malloc.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
25👍9🤯2🥱2🤔1
int не может одновременно быть 1, 2 и 3 — поэтому такой код не срабатывает.
x — одно фиксированное значение, и все сравнения используют один и тот же результат чтения.

#include <stdio.h>

int main() {
int x = 1;
if (x == 1 && x == 2 && x == 3) {
printf("all three are true\n");
}
return 0;
}


Но есть вариант, где это возможно.

volatile int *x = (int *)0x40000000;

if (*x == 1 && *x == 2 && *x == 3) {
// possible
}


Здесь нет одного значения — каждое *x это отдельное чтение из памяти.
Если это указатель на memory-mapped регистр, железо может возвращать разное значение при каждом чтении.

В итоге: 1 → 2 → 3 в рамках одного условия.

upd: упрощённая формулировка была неаккуратной (извини). смысл не в том, что одна и та же программа ведёт себя по-разному, а в том, что такой шаблон выражения:

x == 1 && x == 2 && x == 3


невозможен для хранимого значения, но может стать истинным, если каждое чтение идёт из железа (через *x).
вся суть именно в этом различии между источником значения.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👎13🔥108🤔4👀1
Собственный текстовый редактор: https://viewsourcecode.org/snaptoken/kilo/

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

Из этого кода можно сильно прокачаться просто при чтении.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
16👍3
Понимание оптимизаций компилятора

В этой статье мы разберём, как компиляторы вроде GCC могут преобразовывать на первый взгляд простые операции — такие как x * 2 — в более эффективный машинный код, и разберёмся, какая логика стоит за этими оптимизациями

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
15
Указатели на функции — всё, что нужно знать.
Указатели на функции — это указатели, которые ссылаются на код. При разыменовании полученные данные интерпретируются как инструкции, и процессор их выполняет.

При разыменовании указателя на функцию регистр счётчика команд (PC) в процессоре устанавливается в адрес, на который указывает этот указатель.

Полная заметка доступна здесь: https://pyjamacafe.com/posts/function-pointers/

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
11👍6👀1
Твои переменные в C++ выглядят как cache_size_in_kb или limit_in_mb?

Пользовательские литералы в C++ (UDL), начиная с C++11, делают такие объявления заметно чище:

- Читаемость: 8_mb вместо строки из случайных цифр
- Компонуемость: 8_mb + 512_kb
- Нулевые накладные расходы: вся математика выполняется на этапе компиляции через constexpr
- Отсутствие магических чисел: 8_mb явно задаёт смысл вместо числа 8388608
- Безопасность: можно комбинировать со static_assert, чтобы ловить несовпадения единиц измерения до сборки

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👀53
Реализовали microGPT от Andrej Karpathy с нуля на чистом языке Си: https://github.com/vixhal-baraiya/microgpt-c

Работает на 2.6 млн токенов/с на процессоре Ryzen 5 5600H.

Оптимизация через AVX2 интринсики, ядра скалярного произведения фиксированной ширины (dot16, dot4, dot64) и быструю аппроксимацию экспоненты Шраудольфа.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯108
Можно создать файл размером 4 ГБ в Linux, который почти не занимает места на диске.

В системе он отображается как файл на 4 ГБ. Можно читать любые смещения — в ответ будут нули. При этом реальные блоки на диске не выделяются, пока в файл не начнут записывать данные.

Размер файла — 4 ГБ.
Фактическое использование дискового пространства — около 0.
Оба значения корректны одновременно.

Это называется разрежённый файл (sparse file).

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
12👍7🤔3🥱2
В C есть синтаксис, позволяющий передавать структуру напрямую в функцию без предварительного создания временной переменной.

Во многих легаси-кодовых базах на C до сих пор пишут 4–6 строк шаблонного кода только ради однократного вызова одной функции.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12🥱73👍2
Одно ключевое слово в C сообщает компилятору: эти указатели никогда не пересекаются.

Без него компилятор обязан считать, что любой указатель может алиасить одну и ту же область памяти, и поэтому оптимизировать консервативно.
С ним gcc/clang могут безопасно векторизовать циклы, переупорядочивать загрузки/записи, разворачивать итерации и генерировать SIMD-инструкции.

Это ключевое слово — restrict.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
9👀5🔥4🥱1
Ваш процессор хранит 0x12345678 в памяти как 78 56 34 12.

Само значение не изменилось — изменился порядок байтов. Это называется little-endian.

Большинство современных процессоров (x86, ARM) работают именно так.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱177👍3👎1👀1
Макрос min() в Linux kernel — один из самых тщательно спроектированных макросов вообще.

Стандартный вариант в C:

#define min(a,b) ((a)<(b)?(a):(b))


вычисляет a или b ДВАЖДЫ. Если передать min(x++, y), получите двойной инкремент.

В kernel для этого используется GCC-расширение — statement expressions.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔19🔥64👍2😁1
Однажды компилятор C будто бы опроверг Великую теорему Ферма.

Теорема Ферма утверждает, что для целых чисел невозможно:
a³ + b³ = c³

Её доказал Эндрю Уайлс в 1995 году спустя 358 лет после формулировки.

Но позже Джон Рейгер показал цикл, скомпилированный через gcc -O2, который выводил:

“Fermat's Last Theorem has been disproved.”

Проблема была не в математике, а в undefined behavior.

Вот что на самом деле увидел gcc.

В цикле нет I/O и нет volatile. Стандарт C11 разрешает компилятору предполагать, что такой цикл может завершиться.
Поэтому gcc может считать, что while(1) не обязательно бесконечный. Но проблема была глубже — в undefined behavior.

Переполнение signed int в aaa — это UB. Компилятор исходит из того, что такого переполнения никогда не произойдёт.
С этим допущением он может трансформировать программу так, что логика поиска полностью ломается.

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

Ошибка была в коде, который полагался на undefined behavior.

// что gcc увидел при -O2:
// в цикле нет volatile, нет I/O, нет атомарных операций
// стандарт C11 разрешает считать, что цикл может завершиться
// следовательно: если fermat() завершится, то вернёт только 1
// следовательно: можно сразу вернуть 1
// что примерно сгенерировал компилятор:
int fermat() { return 1; } // не гарантируется, но допустимо при UB


Потом Рейгер решил проверить компилятор. Он добавил printf в конец программы, чтобы вывести найденный контрпример: a b c.

После этого gcc снова начал выполнять реальные вычисления, потому что теперь у программы появился наблюдаемый вывод.
Часть агрессивных оптимизаций исчезла.

Блеф перестал работать.

Позже Рейгер написал:

> “I got the feeling these tools like Fermat himself had not enough room in the margin to explain their reasoning.”

И проблема решается не простым добавлением volatile.
Настоящее решение — не допускать undefined behavior, например переполнения signed int.

volatile лишь запрещает часть оптимизаций, но не исправляет саму ошибку.

// Приём 1: добавление printf заставляет компилятор выполнить вычисления
printf("%d %d %d\n", a, b, c); // теперь компилятор должен их вычислить

// Приём 2: volatile ограничивает оптимизацию
volatile int a = 1, b = 1, c = 1; // чтение теперь — наблюдаемый побочный эффект

// Этот же баг часто встречается во встраиваемой прошивке
// Цикл ожидания без volatile удаляется при -O2
// Процессор пропускает ожидание, что приводит к нерабочему поведению


👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1811🌚3
Одна C-функция положила 10% всего интернета в 1988 году.

функция gets() считывает пользовательский ввод в буфер без ограничения по размеру. что бы ни ввёл пользователь — всё записывается. выходит за пределы буфера, дальше в стек, вплоть до адреса возврата.

аспирант по имени Robert Morris использовал это для переполнения fingerd и инъекции шеллкода. червь разошёлся по 6000 машин за несколько часов.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯17👍63👀2
gcc позволяет запускать код ещё до старта main().

attribute((constructor)) помечает функцию, которая выполняется на этапе загрузки
attribute((destructor)) выполняется после возврата из main()

так shared-библиотеки инициализируют себя при вызове dlopen()

__attribute__((constructor)) void before_main() {
printf("this runs before main\n");
}

int main() {
printf("this runs second\n");
return 0;
}


числовой приоритет — это то, что делает механизм реально управляемым.

меньшее число выполняется раньше, поэтому если две библиотеки регистрируют конструкторы, функция с приоритетом 101 отработает до 102.

glibc резервирует всё ниже 100 под себя
туда лучше не лезть

деструкторы работают в обратном порядке: большее число выполняется раньше при завершении, симметрия сделана специально

__attribute__((constructor(101))) void first() {
printf("runs first\n");
}

__attribute__((constructor(102))) void second() {
printf("runs second\n");
}

int main() {
printf("runs last\n");
return 0;
}


суть работы LD_PRELOAD как раз в этом механизме загрузки разделяемых библиотек.

ты подгружаешь .so в процесс, её конструкторы выполняются до входа в main(), и к моменту старта пользовательского кода таблица символов уже переопределена.

из-за этого можно перехватывать malloc, open, read и любые функции из libc, не модифицируя сам бинарник.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
21👍9🔥3👀1
Кто-то спросил у Биджа, как работают сокеты в C. Ему надоело объяснять это снова и снова. Поэтому в 1995 году он просто выложил всё в интернет.

С тех пор это главное руководство по сетевому программированию на сокетах уже 30 лет.

Там есть всё: TCP, UDP, IPv4, IPv6, неблокирующий ввод-вывод, select(), poll().

Курсы по операционным системам по всему миру включают его в программу. И оно смешнее, чем техническая книга вообще имеет право быть.

Оно бесплатное. И таким останется всегда.

Базовый цикл работы сокетов не менялся с 1995 года. Каждый сервер, к которому ты когда-либо подключался, работает на какой-то версии этой схемы.

Beej буквально показывает, что происходит внутри connect(), accept(), send(), recv().

Как только понимаешь это на C — понимаешь сокеты в любом языке навсегда.

// минимальный TCP-сервер по гайду Beej

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};

bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 10);

int client = accept(sockfd, NULL, NULL);

send(client, "hello\n", 6, 0);

// nginx, redis и большинство серверов начинают с этого паттерна


Зацени. Оно бесплатное и таким останется всегда.
http://beej.us/guide/bgnet

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
21🔥4
Базовый курс по аллокаторам памяти — реализация простого распределителя памяти

Эта статья про написание простого аллокатора памяти на C.
Ты реализуешь malloc(), calloc(), realloc() и free().

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
12👍2