#compiler
Быстрый Switch: таблица адресов💨
В C++ оператор
Оператор
В зависимости от целевой архитектуры, настроек оптимизации, и свойств конкретного
1️⃣ Цепочка последовательных
2️⃣ Таблица адресов (мой перевод), он же branch table, он же jump table.
Первый вариант неинтересен, он самый простой и самый неоптимизированный. Если в нашем
На самом деле в таких случаях компиляторы умеют делать а-ля "бинарный поиск", поэтому вероятно будет log_2(30) сравнений в худшем случае 😁
Во втором варианте номер инструкции, куда надо перепрыгнуть, вычисляется в зависимости от значения переменной, в процессе чего не выполнится ни одного сравнения.
Пример
В этом примере в
Таким образом, можно представить, что это
В примере компилятор сгенерировал метки
Также компилятор сделал таблицу
"Таблица" представляет из себя несколько последовательных 8-байтовых числа, которые являются адресами меток
А метка
Теперь, имея "таблицу адресов", можно вычислить номер инструкции, куда надо прыгать. Если параметр равен 0, то прыгаем по первому адресу таблицы, если 1 - по второму, и так далее.
Компилятор сам определяет, нужна ли таблица адресов. Обычно она используется для "плотных"
Быстрый Switch: таблица адресов
В C++ оператор
switch
используется для передачи потока управления в разные места в зависимости от значения переменной.Оператор
switch
можно представить как соответствие между значениями переменной, и кодом который должен выполниться для каждого значения.В зависимости от целевой архитектуры, настроек оптимизации, и свойств конкретного
switch
-оператора, код может сгенерироваться в разном виде. Есть два варианта, какой ассемблер сгенерирует компилятор:if
-ов. Это самый простой путь, потому что switch
-оператор всегда представим в этом виде.Первый вариант неинтересен, он самый простой и самый неоптимизированный. Если в нашем
switch
30 штук case
-ов, то в худшем случае произойдет 30 (!) последовательных сравнений (цепочка if
-ов), прежде чем программа поймет номер нужной инструкции.Пример
switch
с таблицей адресов: https://godbolt.org/z/3debYb4vqВ этом примере в
switch
сравнивается значение enum
-а. Для компилятора enum
представляется как underlying type. По умолчанию этот тип int
, то есть во всех операциях с enum
происходит неявная конвертация в int
.Таким образом, можно представить, что это
switch
по значениям от 0 до 6 включительно.В примере компилятор сгенерировал метки
LBB0_2
, LBB0_3
, ..., LBB0_8
для каждого соответствующего кода case X
.Также компилятор сделал таблицу
LJTI0_0
, где лежат адреса этих меток. Вообще "таблица" это громко сказано, это просто наша абстракция."Таблица" представляет из себя несколько последовательных 8-байтовых числа, которые являются адресами меток
LBB0_2
-LBB0_8
.А метка
LJTI0_0
указывает на начало последовательности.Теперь, имея "таблицу адресов", можно вычислить номер инструкции, куда надо прыгать. Если параметр равен 0, то прыгаем по первому адресу таблицы, если 1 - по второму, и так далее.
lea rcx, [rip + .LJTI0_0]Отступление: Как известно, метки имеют смысл только для ассемблера. Метка просто условно указывает на позицию в бинарнике (инструкцию или данные). После процесса линковки, когда в один исполняемый файл (бинарник) утрамбуются отдельные объектные файлы, вместо меток появятся нормальные адреса.
movsxd rax, dword ptr [rcx + 4*rax]
add rax, rcx
jmp rax
lea rcx, [rip + 0x012345678]Таблица адресов может иметь другую реализацию, но такая общая идея. Например, в примере с Википедии для рандомного 8-битного ассемблера, значение переменной прибавляется к регистру счетчика команд (
addwf PCL,F
), а сразу после этой инструкции находится таблица с goto до нужной инструкции, и счетчик команд укажет на нужный goto.Компилятор сам определяет, нужна ли таблица адресов. Обычно она используется для "плотных"
switch
, где есть case X
для последовательных значений X
. Если в case X
поставить рандомные значения, то таблицы не получится - пример на godbolt, будут последовательные if
-ы.Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler #madskillz
[[assume]] - помоги компилятору сам😎
Раньше я писал про
Эта штука делает указание компилятору, что в данную ветку исполнения программа никогда не попадет (под личную ответственность программиста), поэтому можно оптимизировать это место.
В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же
Эта штука делает указание компилятору, что в данной ветке исполнения выражение
На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8
Самый простой пример - метод, который делит число на 32:
Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
В более сложных примерах, которые показываются в "предложении", можно в несколько раз уменьшить количество инструкций ассемблера, особенно в математических программах.
Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:
1️⃣ Нужно действительно сильно зависеть от быстродействия программы, например это могут быть реалтаймовые программы. Я однажды кидал видео-выступление Тимура Думлера (автора "предложения") на эту тему - https://www.tgoop.com/cxx95/16.
2️⃣ Нужно понимать, за счет чего срезаются инструкции. Пример программы, которая ограничивает значения массива через std::clamp:
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.
3️⃣ Нужно постоянно лезть в ассемблер скомпилированной программы и проверять результат - а как иначе? И даже нужно делать юнит-тесты на генерируемый ассемблер (я бы по крайней мере делал). У компиляторов C++ много тестов на получающийся ассемблер, и в отдельных программах с assume они тоже нужны.
4️⃣ Стандарт отмечает, что компиляторы сами вольны оптимизировать код как смогут, никаких требований на них не налагается. Надо проверять, как работает отдельный компилятор и даже отдельная версия, для этого нужны юнит-тесты из 3️⃣ пункта
Можно сделать разные приколы с😁
😱 Фиксируем вариант в switch - ссылка на godbolt.
😱 Решаем простые уравнения с переменной - ссылка на godbolt.
[[assume]] - помоги компилятору сам
Раньше я писал про
std::unreachable
(он же __builtin_unreachable
до C++23) - https://www.tgoop.com/cxx95/58.Эта штука делает указание компилятору, что в данную ветку исполнения программа никогда не попадет (под личную ответственность программиста), поэтому можно оптимизировать это место.
В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же
__builtin_assume
до C++23).Эта штука делает указание компилятору, что в данной ветке исполнения выражение
expr
следует считать равным true
, и делать разные оптимизации на основе этих данных. Выражение expr
вычисляться во время работы программы не будет, это подсказка времени компиляции.На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8
Самый простой пример - метод, который делит число на 32:
int div32(int x) {Казалось бы, очевидная оптимизация - не делить на 32, а сделать битовый сдвиг на 5 битов:
return x / 32;
}
int div32(int x) {Но будет неправильно работать на отрицательных числах. Компилятор всегда должен учитывать возможность входа отрицательного числа, из-за этого метод больше по размеру: ссылка на godbolt.
return x >> 5;
}
Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
int div32_2(int x) {И тогда код оптимизируется: ссылка на godbolt.
[[assume(x >= 0)]]; // или __builtin_assume(x >= 0);
return x / 32;
}
В более сложных примерах, которые показываются в "предложении", можно в несколько раз уменьшить количество инструкций ассемблера, особенно в математических программах.
Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:
void limiter(float* data, size_t size) {Предполагая, что размер буфера всегда больше 0 и кратен 32, а флоаты нормализованные, программист ставит assume.
[[assume(size > 0)]];
[[assume(size % 32 == 0)]];
for (size_t i = 0; i < size; ++i) {
[[assume(std::isfinite(data[i]))]];
data[i] = std::clamp(data[i], -1.0f, 1.0f);
}
}
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.
Можно сделать разные приколы с
assume
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Самое простое объяснение std::function за 15 минут😦
Этот пост был написан под влиянием крутого видео от Jason Turner "A Simplified std::function Implementation"
Часто люди не задумываются, как работает
Можно сказать, что в C++ есть два типа объектов, на которых работает семантика вызова как функции. Можно условно назвать их
1️⃣ Сами функции:
2️⃣ Объекты типов с определенным
А
2️⃣ может иметь неопределенный размер. Например, размер структуры у лямбды зависит от того, какие captures он делает.
Поэтому, к сожалению,
Также нужно использовать виртуальный класс, который для каждого отдельного типа как бы вычислит адрес вызываемого метода:
Виртуальный класс и указатель на кучу:
👍
Самое простое объяснение std::function за 15 минут
Этот пост был написан под влиянием крутого видео от Jason Turner "A Simplified std::function Implementation"
Часто люди не задумываются, как работает
std::function
. Чаще всего знают, что эта штука - обертка над чем-то, что можно "вызвать" как функцию. Кто-то смутно помнит, что std::function
вроде как лезет в динамическую память. cppreference не сильно раскрывает внутренности реализации.Можно сказать, что в C++ есть два типа объектов, на которых работает семантика вызова как функции. Можно условно назвать их
Callable
. Это:int foo(int a, int b) { return a + b; }
operator()
, часто их называют "функторы":struct foo {Все остальные
int operator()(int a, int b) { return a + b; }
}
Callable
являются производными от этих двух типов. В том числе лямбды - компилятор их переделывает в структуры с operator()
. Про лямбды есть хорошая книга - https://www.tgoop.com/cxx95/48.А
std::function<Signature>
должен уметь хранить все возможные Callable
с данной сигнатурой. template<typename Ret, typename... Param>Возникает проблема - у
class function<Ret(Param...)> {
// код реализации
};
std::function
должен быть фиксированный размер, но Callable
типа Поэтому, к сожалению,
std::function
хранит Callable
в куче.Также нужно использовать виртуальный класс, который для каждого отдельного типа как бы вычислит адрес вызываемого метода:
Виртуальный класс и указатель на кучу:
struct callable_interface {Реализация для каждого отдельного типа
virtual Ret call(Param...) = 0;
virtual ~callable_interface() = default;
};
std::unique_ptr<callable_interface> callable_ptr;
Callable
держит в себе сам объект Callable
и метод для вызова operator()
по правильному адресу:template<typename Callable>Конструктор
struct callable_impl : callable_interface {
callable_impl(Callable callable_) : callable{std::move(callable_)} {}
Ret call(Param... param) override { return std::invoke(callable, param...); };
Callable callable;
}
std::function
принимает Callable
и создает объект в куче:template<typename Callable>И наконец вызов
function(Callable callable)
: callable_ptr{std::make_unique<callable_impl<Callable>>(std::move(callable))}
{}
operator()
у самого std::function
перенаправляет вызов в содержимый Callable
:Ret operator()(Param... param) { return callable_ptr->call(param...); }Вот так выглядит один из способов type erasure в C++
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
C++ Weekly - Ep 333 - A Simplified std::function Implementation
☟☟ Awesome T-Shirts! Sponsors! Books! ☟☟
Upcoming Workshops:
► C++ Best Practices Workshop, ACCU, Bristol UK, Mar 31, 2025: https://accuconference.org/
T-SHIRTS AVAILABLE!
► The best C++ T-Shirts anywhere! https://my-store-d16a2f.creator-spring.com/…
Upcoming Workshops:
► C++ Best Practices Workshop, ACCU, Bristol UK, Mar 31, 2025: https://accuconference.org/
T-SHIRTS AVAILABLE!
► The best C++ T-Shirts anywhere! https://my-store-d16a2f.creator-spring.com/…
#creepy #compiler
Самое мерзкое правило в C++ для модульных программ и как его обойти🤢
Недавно я в своем pet project снова столкнулся с тем, что могу назвать самым мерзким правилом C++ в своем опыте, по крайней мере для модульных программ. Оно связано с особенностями работы линковщика и требует всяких тайных знания для решения.
В больших модульных проектах для скорости разработки иногда используется такая схема - каждому модулю бизнес-логики соответствует статическая библиотека (static library).
Если какой-то модуль с помощью всяких флагов системы сборки не линковать в итоговый бинарник, то этот модуль не будет билдиться, а в итоговом бинарнике ничего не сломается. Просто в бинарнике будет меньше фичей.
С другой стороны, если модуль слинкован, то он должен как-то "зарегистрировать" себя в списке модулей.😐
Есть файл
Единственный способ, которым это можно адекватно сделать - добавить код, который должен вызываться на старте программы. Это делается через статическую инициализацию объектов в конструкторе объекта. Где-то в одном из
Процесс решения проблемы зависит от системы сборки, операционной системы и многих других вещей. Общепринятого решения проблемы нет. Как эту проблему решают в разных случаях:
1️⃣ Windows - указать на переменные, которых нельзя выбросить - ссылка
2️⃣ Помещение переменных в отдельные секции и пометка этих секций как невыбрасываемых - ссылка
3️⃣ Linux - также указать на невыбрасываемые переменные через параметр командной строки - ссылка1, ссылка2, переменная не должна быть
4️⃣ Linux - сделать link-скрипт, который указывает что и как надо линковать - ссылка
Через время поисков я нашел ответ, почему так работает линкер.
Когда собираешь бинарник, то индивидуальные объектные файлы (
С библиотеками (
В системе сборки CMake есть метод, который позволит обойти это правило. Надо заменить такую строку:
Поэтому "регистрация модуля" сработает и проблема будет решена😁
Самое мерзкое правило в C++ для модульных программ и как его обойти
Недавно я в своем pet project снова столкнулся с тем, что могу назвать самым мерзким правилом C++ в своем опыте, по крайней мере для модульных программ. Оно связано с особенностями работы линковщика и требует всяких тайных знания для решения.
В больших модульных проектах для скорости разработки иногда используется такая схема - каждому модулю бизнес-логики соответствует статическая библиотека (static library).
Если какой-то модуль с помощью всяких флагов системы сборки не линковать в итоговый бинарник, то этот модуль не будет билдиться, а в итоговом бинарнике ничего не сломается. Просто в бинарнике будет меньше фичей.
С другой стороны, если модуль слинкован, то он должен как-то "зарегистрировать" себя в списке модулей.
Есть файл
module.h
с такими методами (упрощенно)struct IModule { virtual void Do() = 0; };Файл
void AddModule(std::unique_ptr<IModule> module);
const std::vector<std::unique_ptr<IModule>>& GetModules();
main.cpp
должен использовать GetModules()
, а модуль должен зарегистрировать сам себя через AddModule
.Единственный способ, которым это можно адекватно сделать - добавить код, который должен вызываться на старте программы. Это делается через статическую инициализацию объектов в конструкторе объекта. Где-то в одном из
.cpp
-файлов модуля должно быть такое:struct Dummy {Дальше начинается кино. Переменная
Dummy() {
AddModule(std::make_shared<MyCoolModule>());
}
};
static Dummy dummy;
dummy
и код для ее инициализации попадает в статическую библиотеку libcoolmodule.a
(можно проверить через objdump), но при линковке бинарника эта переменная выбрасывается линкером как неиспользуемая. В итоге модуль не зарегистрируется.Процесс решения проблемы зависит от системы сборки, операционной системы и многих других вещей. Общепринятого решения проблемы нет. Как эту проблему решают в разных случаях:
__pragma comment(linker,"/include:?variable_name@")
static Var_t g_DumbVar __attribute__((__used__, section(".var_section.g_DumbVar"))) = (const Var_t) X_MARKER;
static Var_t* g_DumbVarGuard[] __attribute__((__used__, section(".guard"))) = { &g_DumbVar };
static
и volatile
.Через время поисков я нашел ответ, почему так работает линкер.
Когда собираешь бинарник, то индивидуальные объектные файлы (
.o
) в команде к линкеру линкуются по порядку и ничто не выбрасывается.С библиотеками (
.a
, архив из .o
) есть "оптимизация": .o
-файл из библиотеки линкуется только если в нем находится определение какого-нибудь undefined symbol, который требуется в уже слинкованных прежде .o
-файлах. В противном случае считается, что этот .o
-файл не нужен и в бинарник он не попадает.В системе сборки CMake есть метод, который позволит обойти это правило. Надо заменить такую строку:
add_library(enum_serializer STATIC module.cpp helper.cpp)на такую:
add_library(enum_serializer OBJECT module.cpp helper.cpp)И тогда, если какой-то бинарник зависит от
enum_serializer
, он будет линковать не libenum_serializer.a
, а module.o
и helper.o
.Поэтому "регистрация модуля" сработает и проблема будет решена
Please open Telegram to view this post
VIEW IN TELEGRAM
#longread
Кодогенератор Waffle++ для C++ 🧇
https://habr.com/ru/post/710744/
Я рассказал о своем проекте расширяемого кодогенератора для С++, где каждый может написать свой модуль быстро и с надежными тестами.
Уже сейчас можно получить бесплатный перевод значений enum в строку и обратно, перевод структуры в JSON и обратно, декларативный веб-сервер, систему слотов и сигналов, свой динамический полиморфизм, генератор кода для тестов...
Это может быть интересно!🥞
https://github.com/Izaron/WafflePlusPlus
Кодогенератор Waffle++ для C++ 🧇
https://habr.com/ru/post/710744/
Я рассказал о своем проекте расширяемого кодогенератора для С++, где каждый может написать свой модуль быстро и с надежными тестами.
Уже сейчас можно получить бесплатный перевод значений enum в строку и обратно, перевод структуры в JSON и обратно, декларативный веб-сервер, систему слотов и сигналов, свой динамический полиморфизм, генератор кода для тестов...
Это может быть интересно!
https://github.com/Izaron/WafflePlusPlus
Please open Telegram to view this post
VIEW IN TELEGRAM
#advertisement
Работа на C++ в Алисе🎙
Алиса это один из самых быстрорастущих сервисов Яндекса, где можно заниматься уникальными задачами на C++ - от embedded-разработки до машинного обучения. Я сам работаю над Алисой уже несколько лет и могу это гарантировать🦆
Сейчас Алиса расширяется и нанимает много разработчиков на C++ (от стажеров до сениоров), поэтому я решил помочь коллегам найти крутых разработчиков из читателей этого блога.
Пишите мне (@cloudy_district) если захотите попробовать себя в разработке сервиса с миллионами пользователей!😀
Работа на C++ в Алисе
Алиса это один из самых быстрорастущих сервисов Яндекса, где можно заниматься уникальными задачами на C++ - от embedded-разработки до машинного обучения. Я сам работаю над Алисой уже несколько лет и могу это гарантировать
Сейчас Алиса расширяется и нанимает много разработчиков на C++ (от стажеров до сениоров), поэтому я решил помочь коллегам найти крутых разработчиков из читателей этого блога.
Пишите мне (@cloudy_district) если захотите попробовать себя в разработке сервиса с миллионами пользователей!
Please open Telegram to view this post
VIEW IN TELEGRAM
#story #compiler
Порог входа для коммитов в компиляторы🍗
Однажды в одном чате про C++ один участник высказал сомнение - имеет ли смысл делать багфиксы в существующие компиляторы, разве там нет "проблем" с тем, что желающих исправить огромно, и issues просто моментально исчезают?😱
На деле ситуация с Clang/LLVM (и много где еще) - обратная.
Тысячи висящих issue, которых никто годами не исправляет. Ревью тоже медленное, pull request висят неделями-месяцами. Активных разработчиков - максимум несколько десятков, и то там большой перекос в единицы супер-активных.🏃♂️
Многие из упомянутого актива работают "на зарплате", то есть работают в Google/Apple/%company_name% и за деньги разрабатывают Clang/LLVM, иначе наверное разработка вообще не будет двигаться.
Кроме багфиксов, реально каждый может зайти на https://clang.llvm.org/cxx_status.html#cxx20, посмотреть на то, что из C++20/23 не реализовано в Clang и поддержать новую фичу. Я такое делал пару раз. Но это были простые коммиты. Сложные предложения реализовать почти нереально, это нужно делать как фултайм работу. Поэтому не только лишь все это делают.
Например, сравнительно просто можно сделать фичу C++23 [P2324R2] Labels at the end of compound statements. У меня ушло 2 коммита - коммит 1, коммит 2.
Также какие-то подвижки можно сделать для [P0533R9] constexpr for <cmath> and <cstdlib>. В этом предложении куча новых методов помечена как
Запись
Также я добавил тест, в котором можно следить за реализацией
Однако, спустя несколько месяцев, дело продвинулось лишь ненамного - blame теста, видно всего два коммита. Там пацан, который в основном занимается libcxx, поддержал часть функций.
Поэтому контрибьютить в компиляторы сможет любой, если есть время и желание сделать что-то новое🚬
Порог входа для коммитов в компиляторы
Однажды в одном чате про C++ один участник высказал сомнение - имеет ли смысл делать багфиксы в существующие компиляторы, разве там нет "проблем" с тем, что желающих исправить огромно, и issues просто моментально исчезают?
На деле ситуация с Clang/LLVM (и много где еще) - обратная.
Тысячи висящих issue, которых никто годами не исправляет. Ревью тоже медленное, pull request висят неделями-месяцами. Активных разработчиков - максимум несколько десятков, и то там большой перекос в единицы супер-активных.
Многие из упомянутого актива работают "на зарплате", то есть работают в Google/Apple/%company_name% и за деньги разрабатывают Clang/LLVM, иначе наверное разработка вообще не будет двигаться.
Кроме багфиксов, реально каждый может зайти на https://clang.llvm.org/cxx_status.html#cxx20, посмотреть на то, что из C++20/23 не реализовано в Clang и поддержать новую фичу. Я такое делал пару раз. Но это были простые коммиты. Сложные предложения реализовать почти нереально, это нужно делать как фултайм работу. Поэтому не только лишь все это делают.
Например, сравнительно просто можно сделать фичу C++23 [P2324R2] Labels at the end of compound statements. У меня ушло 2 коммита - коммит 1, коммит 2.
Также какие-то подвижки можно сделать для [P0533R9] constexpr for <cmath> and <cstdlib>. В этом предложении куча новых методов помечена как
constexpr
. Я придумал схему для реализации - метод должен быть помечен как constexpr, если его константное вычисление поддерживается компилятором. Условный пример для std::fmax:inline _LIBCPP_CONSTEXPR_CXX23_IF_CONSTEXPR_BUILTIN(__builtin_fmax) double fmax(double __x, double __y) {Макрос
return __builtin_fmax(__x, __y);
}
_LIBCPP..._BUILTIN
внутри себя обращается к функции __has_constexpr_builtin
(из моего коммита) для проверки, поддерживается ли функция в аргументе в константном вычислении.Запись
__builtin_XXX
в рантайм-вычислении обычно преобразуется в просто вызов метода/какой-то код, а в константном вычислении выполняется прямо в компиляторе. Для каждой функции нужен свой коммит в компилятор (мой коммит для fmax).Также я добавил тест, в котором можно следить за реализацией
P0533
(мой коммит). Как только какой-то метод помечается как constexpr (автоматическим образом как выше), обязательно нужно поменять макрос в тесте (иначе тест упадет). Как только все нужные методы помечены как constexpr, то тест скажет что фича P0533
полностью реализована (и надо поменять статус на cxx_status.html
).Однако, спустя несколько месяцев, дело продвинулось лишь ненамного - blame теста, видно всего два коммита. Там пацан, который в основном занимается libcxx, поддержал часть функций.
Поэтому контрибьютить в компиляторы сможет любой, если есть время и желание сделать что-то новое
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Чем исключения в C++ похожи на сборщик мусора в других языках ♻️
Имеется в виду не техническая похожесть, а сам принцип наличия фичи. Изначально все фичи выглядят как что-то крутое, что есть "из коробки" и можно использовать без задней мысли.
Но когда возникает нужда что-то поправить в этой фиче, то программист замучается глотать пыль, копаясь в технической реализации.🔍
Например, для Java и других языков со сборкой мусора это необходимость разбираться в алгоритмах сборки мусора (mark-and-sweep, etc.), в более хитрой работе с памятью чтобы сборщик собирал меньше мусора ("пул объектов"), понимать нужные настройки чтобы сборщик не фризил программу посередине HTTP-запроса, и так далее. Это все долго и нудно описывается в разных статьях.
В C++ есть подобная история с исключениями. Но здесь требуются еще более нетривиальные познания для полного понимания. Основных проблем две, первая встречается чаще, но вторая более жесткая:
1️⃣ Оверхед по времени исполнения и хуже с компиляторными оптимизациями. Есть супер крутой лонгрид Исключения C++ через призму компиляторных оптимизаций.
Вместо того, чтобы просто взять и вызвать функцию, которая теоретически может выбросить исключение, приходится делать дополнительный блок кода для обработки потенциального исключения, и переводить исполнение туда в случае исключения (лишняя проверка на КАЖДЫЙ вызов).
Также во многих оптимизациях исключения не участвуют. Например, оптимизация "удаление мертвого кода" считает, что код для обработки исключений нужен всегда, а там по транзитивности нужны и все переменные внутри него, и в итоге все вообще плохо оптимизируется.
2️⃣ Сложность в разработке, в том числе в обеспечении Strong Exception Guarantee.
Strong Exception Guarantee это гарантия, что если вызвать метод, который вдруг выбросит исключение, то состояние программы будет таким же, как было до вызова метода. Контейнеры по возможности реализуют SEG, и это очень муторно. В моей статье про контейнеры я описывал, когда работает SEG.
В обычных программах сделать нормальный SEG очень сложно. Не работает история о том, что мы приняли запрос, положили из него данные в структуру, что-то записали в базу данных, залогировали, а потом вдруг поймали исключения и надо все вернуть обратно.
Есть понятие гарантии Basic Exception Guarantee, программа продолжает работать, но данные немного поломаны. Например, в примере выше, в зависимости от свойств класса😐
Объяснение сути BEG на примере саппорта Microsoft Word (😢 - клиент, 🎧 - саппорт):
😢 - Это поддержка MS Word? Я писал книгу, но Word вдруг все удалил.
🎧 - Все нормально, просто мы предоставляем только базовые гарантии при ошибках программы.
😢 - Большое спасибо, удачного дня!
Во многих code style (например от Google) исключения использовать запрещено.
В компиляторах есть флаг
Чем исключения в C++ похожи на сборщик мусора в других языках ♻️
Имеется в виду не техническая похожесть, а сам принцип наличия фичи. Изначально все фичи выглядят как что-то крутое, что есть "из коробки" и можно использовать без задней мысли.
Но когда возникает нужда что-то поправить в этой фиче, то программист замучается глотать пыль, копаясь в технической реализации.
Например, для Java и других языков со сборкой мусора это необходимость разбираться в алгоритмах сборки мусора (mark-and-sweep, etc.), в более хитрой работе с памятью чтобы сборщик собирал меньше мусора ("пул объектов"), понимать нужные настройки чтобы сборщик не фризил программу посередине HTTP-запроса, и так далее. Это все долго и нудно описывается в разных статьях.
В C++ есть подобная история с исключениями. Но здесь требуются еще более нетривиальные познания для полного понимания. Основных проблем две, первая встречается чаще, но вторая более жесткая:
Вместо того, чтобы просто взять и вызвать функцию, которая теоретически может выбросить исключение, приходится делать дополнительный блок кода для обработки потенциального исключения, и переводить исполнение туда в случае исключения (лишняя проверка на КАЖДЫЙ вызов).
Также во многих оптимизациях исключения не участвуют. Например, оптимизация "удаление мертвого кода" считает, что код для обработки исключений нужен всегда, а там по транзитивности нужны и все переменные внутри него, и в итоге все вообще плохо оптимизируется.
Strong Exception Guarantee это гарантия, что если вызвать метод, который вдруг выбросит исключение, то состояние программы будет таким же, как было до вызова метода. Контейнеры по возможности реализуют SEG, и это очень муторно. В моей статье про контейнеры я описывал, когда работает SEG.
void TryAddWidget(std::vector<Widget>& widgets) {В Стандарте есть велосипеды специально для SEG: std::move_if_noexcept.
// выполняем некий код...
Widget w;
try {
widgets.push_back(std::move(w));
} catch (...) {
// поймали исключение...
// ожидаем, что `widgets` остался юзабельным
}
}
В обычных программах сделать нормальный SEG очень сложно. Не работает история о том, что мы приняли запрос, положили из него данные в структуру, что-то записали в базу данных, залогировали, а потом вдруг поймали исключения и надо все вернуть обратно.
Есть понятие гарантии Basic Exception Guarantee, программа продолжает работать, но данные немного поломаны. Например, в примере выше, в зависимости от свойств класса
Widget
, в векторе widgets
могут стереться все элементы после выброса исключения Объяснение сути BEG на примере саппорта Microsoft Word (
Во многих code style (например от Google) исключения использовать запрещено.
В компиляторах есть флаг
-fno-exceptions
, которые удаляют весь оверхед, связанный с исключениями, не генерируют лишний код. Исключения до сих пор можно будет бросать, но они просто будут крашить программу, и никак не будут обрабатываться.Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy
char, который не char👎
В файлах разных форматах встречаются file signature - это какая-то последовательность байт, которая помогает подтвердить точный формат файла. Часто это значит, что в начале файла расположены "магические байты" специально для этого формата.
На википедии есть список сигнатур для многих форматов. Часто байты в начале файла человекочитаемы, то есть байты кодируют буквы латиницы - например
Посмотрим на пример класса, который принимает четыре байта, а потом проверяет, является ли это сигнатурой RAR-файла:
Запись литерала
Большинство компиляторов C++ поддерживают мульти-символьный литерал и переводят его в int как если бы это были последовательные байты.
Ссылка на godbolt.
Вот такой бывает удобный юзкейс, когда надо взаимодействовать с человекочитаемыми сигнатурами в бинарных файлах😐
char, который не char
В файлах разных форматах встречаются file signature - это какая-то последовательность байт, которая помогает подтвердить точный формат файла. Часто это значит, что в начале файла расположены "магические байты" специально для этого формата.
На википедии есть список сигнатур для многих форматов. Часто байты в начале файла человекочитаемы, то есть байты кодируют буквы латиницы - например
Rar!
, LZIP
, OggS
.Посмотрим на пример класса, который принимает четыре байта, а потом проверяет, является ли это сигнатурой RAR-файла:
class BinarySignature {Заметили ли вы что-то страшное? Это сравнение
public:
BinarySignature(int32_t value)
: Value_{value}
{}
int32_t AsInt() {
return Value_;
}
bool IsRar() {
return Value_ == 'Rar!';
}
private:
int32_t Value_;
};
int
с многосимвольным char
!Value_ == 'Rar!'Запись литерала
'X'
из одного символа везде поддерживается одинаково и имеет тип char
.Запись литерала
'XXXXX'
из нескольких символов имеет тип int
, но компилятор вправе не поддерживать такую запись. Также у этой записи implementation-defined числовое значение, то есть также отдано на откуп компилятору.Большинство компиляторов C++ поддерживают мульти-символьный литерал и переводят его в int как если бы это были последовательные байты.
Ссылка на godbolt.
Вот такой бывает удобный юзкейс, когда надо взаимодействовать с человекочитаемыми сигнатурами в бинарных файлах
Please open Telegram to view this post
VIEW IN TELEGRAM
old-blog.png
168.5 KB
#offtop
Обновление izaron.github.io🔺
На этом скриншоте - старая версия сайта сайта izaron.github.io, которая велась 5-6 лет назад (сделал скриншот перед обновлением).
Я решил перевести все посты на английский и обновить сайт. Обнаружил, что за прошедшие 6 лет доступные шаблоны для личного блога стали приближенными к магии✨
В постах с разметкой markdown есть такие фичи:
- можно писать LaTeX-формулы
- встраивать видео с YouTube
- с полпинка прикрутить систему комментариев и реакций
- есть адаптивная верстка, красивая подсветка кода, темная и светлая тема
- сайт обновляется сразу, как только запушить новый коммит на GitHub
- большие возможности по кастомизации всяких кнопок и внешнего вида
Шаблон Chirpy крутой, мне нравится😁 Можно скопипастить мой репозиторий блога.
Обновление izaron.github.io
На этом скриншоте - старая версия сайта сайта izaron.github.io, которая велась 5-6 лет назад (сделал скриншот перед обновлением).
Я решил перевести все посты на английский и обновить сайт. Обнаружил, что за прошедшие 6 лет доступные шаблоны для личного блога стали приближенными к магии
В постах с разметкой markdown есть такие фичи:
- можно писать LaTeX-формулы
- встраивать видео с YouTube
- с полпинка прикрутить систему комментариев и реакций
- есть адаптивная верстка, красивая подсветка кода, темная и светлая тема
- сайт обновляется сразу, как только запушить новый коммит на GitHub
- большие возможности по кастомизации всяких кнопок и внешнего вида
Шаблон Chirpy крутой, мне нравится
Please open Telegram to view this post
VIEW IN TELEGRAM
#creepy
std::move_only_function - самая позорная фича C++🤡
В С++23 добавили std::move_only_function. Это тот же std::function, но в нем основное различие - нет copy-конструкторов, то есть с объектом можно сделать только move.
(Недавно был пост про самую простую реализацию std::function).
В чем кринж этой фичи? Это НЕ добавление "нового класса", с разницей как между
Это просто "улучшение" старого
Почему я думаю, что это улучшение старого класса, а не новый класс:
1️⃣ Copy-конструкторы в
2️⃣ В новом классе есть фичи наподобии "small string optimization" - Callable-объект могут не пихать в динамическую память, если у них маленький размер. Это очень нужно, большинство Callable имеют малый размер.
3️⃣ Об этом написали сами авторы класса в своем пропозале - что они фиксят разные мелкие баги
То есть Комитет по C++, не имея возможности и воли решить вопрос со сломом ABI, решает дублировать классы со стремными названиями и делает вид что так и должно быть. Это не нормально и вызывает у всех много вопросов. Еще на юзеров перекладывается обязанность переписать используемый тип, вместо того чтобы просто обновить версию
Для почти всех классов STL есть идеи по улучшению, и если просто делать новые классы, то это будет жесть.
Можно везде использовать
std::move_only_function - самая позорная фича C++
В С++23 добавили std::move_only_function. Это тот же std::function, но в нем основное различие - нет copy-конструкторов, то есть с объектом можно сделать только move.
(Недавно был пост про самую простую реализацию std::function).
В чем кринж этой фичи? Это НЕ добавление "нового класса", с разницей как между
std::string
и std::string_view
.Это просто "улучшение" старого
std::function
. Если бы std::function
можно было нормально менять, то он бы выглядел, как сегодняшний std::move_only_function
. Но менять его нельзя, по причинам описанным в посте про ABI.Почему я думаю, что это улучшение старого класса, а не новый класс:
std::function
и так не нужны совсем. Как минимум это бесполезно, как максимум это создает разные проблемы при ненамеренном копировании функторов.std::function
.То есть Комитет по C++, не имея возможности и воли решить вопрос со сломом ABI, решает дублировать классы со стремными названиями и делает вид что так и должно быть. Это не нормально и вызывает у всех много вопросов. Еще на юзеров перекладывается обязанность переписать используемый тип, вместо того чтобы просто обновить версию
libstdc++
.Для почти всех классов STL есть идеи по улучшению, и если просто делать новые классы, то это будет жесть.
Можно везде использовать
std::move_only_function
вместо std::function
, но лучше бы std::function
был переделан в нормальный вид без нового класса.Please open Telegram to view this post
VIEW IN TELEGRAM
#opensource
Обзор на GNOME🦶
Я сделал хештег
—
GNOME это окружение рабочего стола, одно из двух самых популярных наравне с KDE.
Денисы Поповы наделали кучу его форков:BolgenOS, MATE, Cinnamon, Pantheon, Consort, etc.
GNOME или его форк используются по умолчанию в куче дистрибутивов: Ubuntu, Debian, Fedora, Arch, Linux Mate, openSUSE, etc.
Мое знакомство с GNOME началось с того, что его UI мне очень нравился, а UI у KDE - категорически нет. Поэтому я решил помочь GNOME патчами.
6 лет назад патчи отсылались поголубиной электронной почте. За это время, видимо, у последнего мейнтейнера умер его Pentium, который тянул только почтовый клиент, поэтому сейчас завели GitLab.
Так выглядят патчи в типичный проект Gnome - nautilus (файловый менеджер), на примере моих коммитов туда:
1️⃣ Фикс группового переименования директорий
2️⃣ Вроде бы фикс popup-а для двух мониторов
3️⃣ Подтверждение смены имени файла при конфликтах через Enter
Бездна баттхёрта начинается с того, что почти все проекты Gnome написаны на Си. Так выглядит обычная структура типа очереди:
Программирование на Си занятие специфичное. По заветам дедушки Ленина там бесклассовое общество. Поэтому приходится вызывать длинные функции
Активно используется уникальная идиома Си - opaque data type, например для hash table.
В этой идиоме пользователь видит просто объявление структуры
Программировать на Си мне не понравилось, так как все равно нужно сначала думать в терминах ООП (как в C++), а потом переводить мысли в Си как через перевод Гоблина.
Самая страшная вещь это то, что Gnome - радикальные велосипедисты. У них есть такие велосипеды, куда вложено куча усилий, как:
1️⃣ GObject - жуткая имитация ООП "как в C++", лишь бы не писать на C++.
2️⃣ Vala - новый язык программирования для десктопных приложений Gnome. Вяло разрабатывается с 2006. Код на нем транслируется в Си. Зачем он сделан - решительно непонятно. У Gnome (точнее у GTK) есть куча обвязок в другие языки, разрабатывать приложения можно на Python, C++, JavaScript, ...
3️⃣ Builder - новая IDE, также непонятно зачем нужная. Косят под Xcode? Разрабатывать приложения можно из любой IDE, где фичей будет заведомо больше.
Для Gnome я бы отметил, что там по ощущениям вроде как всё плохо с тестами. По истории коммитов в nautilus видно, что там половина коммитов - переводы (в основном на такие важные языки как Friulian, Occitan, Catalan, Faroese...), другая половина - изменение поведения принципиально без автотестов.
Обзор на GNOME
Я сделал хештег
#opensource
, в котором будут обзоры на opensource проекты с уклоном в C/C++. Иногда интересно поисследовать исходники и даже сделать туда патчи, чтобы узнать много нового.—
GNOME это окружение рабочего стола, одно из двух самых популярных наравне с KDE.
Денисы Поповы наделали кучу его форков:
GNOME или его форк используются по умолчанию в куче дистрибутивов: Ubuntu, Debian, Fedora, Arch, Linux Mate, openSUSE, etc.
Мое знакомство с GNOME началось с того, что его UI мне очень нравился, а UI у KDE - категорически нет. Поэтому я решил помочь GNOME патчами.
6 лет назад патчи отсылались по
Так выглядят патчи в типичный проект Gnome - nautilus (файловый менеджер), на примере моих коммитов туда:
Бездна баттхёрта начинается с того, что почти все проекты Gnome написаны на Си. Так выглядит обычная структура типа очереди:
struct NautilusFileQueue
{
GList *head;
GList *tail;
GHashTable *item_to_link_map;
};
GList выглядит так же стремно со всеми вытекающими:typedef struct _GList GList;
struct _GList
{
gpointer data;
GList *next;
GList *prev;
};
Это вызывает флешбеки к задачам с leetcode, там тоже надо было вручную переворачивать списки.Программирование на Си занятие специфичное. По заветам дедушки Ленина там бесклассовое общество. Поэтому приходится вызывать длинные функции
<имя-модуля>_<имя-класса>_<имя-метода>(<аргументы>)
Это мешает автокомплиту, который не может найти нужную функцию из миллиона других.Активно используется уникальная идиома Си - opaque data type, например для hash table.
В этой идиоме пользователь видит просто объявление структуры
struct foo;
и функции которые первым аргументом берут struct foo*
, и на этом всё.Программировать на Си мне не понравилось, так как все равно нужно сначала думать в терминах ООП (как в C++), а потом переводить мысли в Си как через перевод Гоблина.
Самая страшная вещь это то, что Gnome - радикальные велосипедисты. У них есть такие велосипеды, куда вложено куча усилий, как:
Для Gnome я бы отметил, что там по ощущениям вроде как всё плохо с тестами. По истории коммитов в nautilus видно, что там половина коммитов - переводы (в основном на такие важные языки как Friulian, Occitan, Catalan, Faroese...), другая половина - изменение поведения принципиально без автотестов.
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Встраивание файлов в исходники 📦
Иногда удобнее, чтобы бинарь не загружал какие-то файлы из файловой системы, а имел их встроенными прямо в исходный код во время компиляции. Кому это может быть нужно, из разных сфер:
⭕️ Финтех: много коэффициентов и числовых констант для performance-critical алгоритмов
⭕️ Геймдев: иконки, текстуры, код шейдеров и скриптов
⭕️ Embedded: часто это единственный вариант, если микросхема не имеет ОС и соответственно файловой системы
⭕️ Бэкенд: файлы настроек (известных в build-time), SSL/TLS-сертификаты
Чаще всего для такой цели используется программа
Посмотрим на пример: файл template.cpp - это шаблон для генерации кода.
Запустим команду
Подобную функциональность несколько лет пытаются внести в C/C++ в виде директивы препроцессора
Встраивание файлов в исходники 📦
Иногда удобнее, чтобы бинарь не загружал какие-то файлы из файловой системы, а имел их встроенными прямо в исходный код во время компиляции. Кому это может быть нужно, из разных сфер:
Чаще всего для такой цели используется программа
xxd
.Посмотрим на пример: файл template.cpp - это шаблон для генерации кода.
Запустим команду
xxd -i template.cpp template.cpp.dataПолучим такой файл template.cpp.data:
unsigned char template_cpp[] = {Потом этот файл можно подключить и сделать из него строку (надо указать длину, так как байты не нуль-терминированы):
/* байты */
}
unsigned int template_cpp_len = /* кол-во байтов */;
#include "template.cpp.data"В системе сборки можно автоматизировать, чтобы команда
const std::string TEMPLATE{(char*)template_cpp, template_cpp_len};
xxd
запускалась автоматически каждый раз при изменении шаблона, и сгенерированный файл не попадал в исходники (то есть лежал в build-директории): ссылка на функцию CMake.Подобную функциональность несколько лет пытаются внести в C/C++ в виде директивы препроцессора
#embed
. Пока удалось это сделать для C23 - крутой блог с примерами:static const char sound_signature[] = {
#embed <sdk/jump.wav>
};
// verify PCM WAV resource signature
assert(sound_signature[0] == 'R');
assert(sound_signature[1] == 'I');
assert(sound_signature[2] == 'F');
assert(sound_signature[3] == 'F');
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Кина не будет: цирк в комитете по C++ 🤹
Как я писал, в C++23 приняли аттрибут [[assume(expr)]]. Просматривая статус поддержки C++23 в Clang, я увидел что этот аттрибут пока не поддержан.
Я отправил патч на его поддержку - https://reviews.llvm.org/D144334
Это заняло мало времени и всего несколько строк в коде (не считая тестов и документации), потому что в Clang уже есть
Часть переписки по патчу:
erichkeane:
Кина не будет: цирк в комитете по C++ 🤹
Как я писал, в C++23 приняли аттрибут [[assume(expr)]]. Просматривая статус поддержки C++23 в Clang, я увидел что этот аттрибут пока не поддержан.
Я отправил патч на его поддержку - https://reviews.llvm.org/D144334
Это заняло мало времени и всего несколько строк в коде (не считая тестов и документации), потому что в Clang уже есть
__builtin_assume
, который сделан аналогично:case attr::Assume: {В ревью пришло несколько человек и началась клоунада. Оказывается, что представители Clang и MSVC на встречах комитета по стандартизации заявляли, что отказываются реализовывать эту фичу по разным причинам. Причины понятны - плохо полагаться на не описанное в стандарте поведение компилятора (в своем посте я описывал опасности).
llvm::Value *ArgValue = EmitScalarExpr(cast<AssumeAttr>(A)->getCond());
llvm::Function *FnAssume = CGM.getIntrinsic(llvm::Intrinsic::assume);
Builder.CreateCall(FnAssume, ArgValue);
break;
}
Часть переписки по патчу:
erichkeane:
So one thing to note here: I'm on the fence as to whether we want to implement this feature at all. As was discussed extensively during the EWG meetings on this: multiple implementers are against this attribute for a variety of reasons, and at least 1 other implementer has stated they might 'implementer veto' this. I think there is discussion to be had among the code owners here as to whether we even want this.
Izaron: I don't quite understand how it works. The feature has been approved for C++2b, but it should have not been approved if there were concerns from implementers. <...> Could you please elaborate: if you decide to not implement this feature, you will kind of revoke the proposal or just deliberately do not support a part of C++2b in Clang?
erichkeane: Just deliberately not support a part of C++2b. Implementers have veto'ed features in the past exactly that way.
aaron.ballman: Agreed, (IMO) it should not have been approved given how many implementer concerns were expressed. But what goes into the standard is whatever gains consensus in the committee, so the committee occasionally runs the risk of voting in something that won't be implemented. We try to avoid it whenever possible, but it still happens with some regularity.
Вот так! Деды из комитета могут принимать любые изменения, которые Clang и/или GCC и/или MSVC никогда не реализуют, просто по большинству голосов. Комитет всегда умеет удивить.#opensource
Обзор на Lua👩💻
Lua это классический скриптовый язык, широко известный в некоторых кругах. На нем пишутся аддоны к World of Warcraft, Nmap, Nginx, Adobe Lightroom, Neovim, и еще к сотне других проектов. Я решил сделать обзор и собрал всякую редкую информацию.
Этот язык простой как пробка. Основу можно узнать в Learn Lua in 15 minutes.
В языке единственная структура данных это хэш-таблица. Там есть многочисленный синтаксический сахар, то есть эти записи:
Через эти хэш-таблицы имитируется абсолютно всё с использованием разного рода костылей.
Даже можно реализовать, с позволения сказать, ООП.
Объекту
Если какого-то поля
В языке есть корутины, closure (как лямбда-функции в C++), рефлексия, и прочие нужные приколы.
В интернете есть многие сотни статей про Lua, даже я написал статью 10 лет назад, но лучше читать книгу от автора Programming in Lua. В книгах обычно самое полное изложение, в то время как статьи в интернете заведомо неполные и обычно пишутся чтобы "показать чето крутое".
В книге есть такая информация, которой больше нигде нет, например:
1️⃣ Разные флаги, например флаг
2️⃣ Описание условий tail call optimization у функций
3️⃣ Метки и goto
4️⃣ Особенность сборки мусора
Изначально Lua состоял только из интерпретатора и годился для интеракции с проектами на C/C++ (хотя Lua можно использовать и сам по себе как самостоятельный язык).
За долгое время накопилась куча библиотек и проектов. Есть даже несколько новых интерпретаторов/компиляторов, то есть от авторского Lua там только синтаксис языка. Есть полуживой менеджер пакетов LuaRocks (я туда делал пару коммитов).
Конечно, с Python объем всего добра не сравнится, но по сравнению с другими скриптовыми языками Lua выглядит хорошо.
Интересно, что в языке есть навороченный сборщик мусора. Это реализация обычного mark-and-sweep, с крутыми особенностями:
1️⃣ Раньше сборщик мусора делал
Каждый раз, когда нужно аллоцировать память в
В итоге не происходит никаких "фризов", просто аллокация памяти выглядит чуть замедленной.
2️⃣ На объект (= хэш-таблицу) можно повесить функцию, которая вызовется перед "удалением" этого объекта сборщиком мусора. Прикол в том, что внутри этой функции можно сохранить объект в какую-нибудь переменную, и удаления в итоге не произойдет. Это называется воскрешением объекта ✝️ . Такого нет во многих языках.
В книге есть разные примеры использования этой техники.
3️⃣ В таблице можно пометить все ключи и/или значения как "weak". Тогда сборщик мусора не будет считать такие ссылки за настоящие ("strong") и в при удалении объекта удалит протухшую пару ключ-значение из таблицы.
Кстати, у Вани в канале есть сборник постов о GC, можно подписаться и почитать😐
Интерпретатор Lua работает так - читает исходник, транслирует его в "байткод" и интерпретирует этот байткод (как в Java), это быстрее и удобнее.
Поисследовать этот процесс можно, скомпилировав исходники Lua в debug-режиме и запуская его из-под gdb.
Лексер (перевод кусков кода в "токены") и парсер (перевод "токенов" в байткод) работают одновременно, трансляция происходит в один проход, достаточно смотреть на следующий токен (это LL(1)-парсер).
Это самый простой транслятор, и наверное каждый смог бы реализовать перевод Lua в байткод.
Обзор на Lua
Lua это классический скриптовый язык, широко известный в некоторых кругах. На нем пишутся аддоны к World of Warcraft, Nmap, Nginx, Adobe Lightroom, Neovim, и еще к сотне других проектов. Я решил сделать обзор и собрал всякую редкую информацию.
Этот язык простой как пробка. Основу можно узнать в Learn Lua in 15 minutes.
В языке единственная структура данных это хэш-таблица. Там есть многочисленный синтаксический сахар, то есть эти записи:
foo.bar = 1337... аналогичны этим:
function Lib.sum (x, y) return x + y end
list = {"apple", "orange", "banana"}
foo["bar"] = 1337... то есть "массив" это тоже хэш-таблица с ключами от
Lib["sum"] = function (x, y) return x + y end
list = {[1] = "apple", [2] = "banana", [3] = "orange"}
1
до n
(нумерация массивов в Lua с единицы)Через эти хэш-таблицы имитируется абсолютно всё с использованием разного рода костылей.
Даже можно реализовать, с позволения сказать, ООП.
Объекту
foo
(хэш-таблице) можно придать ссылку __index
на базовый класс (другую хэш-таблицу).Если какого-то поля
foo.bar
(ключа bar
в таблице foo
) нет, то интерпретатор Lua посмотрит в таблицу foo.__index
, а если и там нет, то в foo.__index.__index
, и так далее.В языке есть корутины, closure (как лямбда-функции в C++), рефлексия, и прочие нужные приколы.
В интернете есть многие сотни статей про Lua, даже я написал статью 10 лет назад, но лучше читать книгу от автора Programming in Lua. В книгах обычно самое полное изложение, в то время как статьи в интернете заведомо неполные и обычно пишутся чтобы "показать чето крутое".
В книге есть такая информация, которой больше нигде нет, например:
LUA_32BITS
скомпилирует интерпретатор "Small Lua" с 32-битными числамиИзначально Lua состоял только из интерпретатора и годился для интеракции с проектами на C/C++ (хотя Lua можно использовать и сам по себе как самостоятельный язык).
За долгое время накопилась куча библиотек и проектов. Есть даже несколько новых интерпретаторов/компиляторов, то есть от авторского Lua там только синтаксис языка. Есть полуживой менеджер пакетов LuaRocks (я туда делал пару коммитов).
Конечно, с Python объем всего добра не сравнится, но по сравнению с другими скриптовыми языками Lua выглядит хорошо.
Интересно, что в языке есть навороченный сборщик мусора. Это реализация обычного mark-and-sweep, с крутыми особенностями:
stop-the-world
(когда посреди исполнения программа останавливается и сборщик собирает весь мусор), а сейчас сборщик инкрементальный.Каждый раз, когда нужно аллоцировать память в
N
байт, сборщик мусора заодно делает небольшой объем работы, прямо пропорциональный этому N
.В итоге не происходит никаких "фризов", просто аллокация памяти выглядит чуть замедленной.
В книге есть разные примеры использования этой техники.
Кстати, у Вани в канале есть сборник постов о GC, можно подписаться и почитать
Интерпретатор Lua работает так - читает исходник, транслирует его в "байткод" и интерпретирует этот байткод (как в Java), это быстрее и удобнее.
Поисследовать этот процесс можно, скомпилировав исходники Lua в debug-режиме и запуская его из-под gdb.
Лексер (перевод кусков кода в "токены") и парсер (перевод "токенов" в байткод) работают одновременно, трансляция происходит в один проход, достаточно смотреть на следующий токен (это LL(1)-парсер).
Это самый простой транслятор, и наверное каждый смог бы реализовать перевод Lua в байткод.
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler
Теория девиртуализации😶
Виртуальные функции обычно работают через vtable. Если метод виртуальный, то вместо вызова точно известного метода, в рантайме вычисляется адрес метода, который зависит от динамического типа объекта.
Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный😐
Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память.
1️⃣ Метод класса помечен как
Смысл в том, что даже в случае работы с объектами
2️⃣ Класс является финальным. Смысл в том, что указатель на этот класс не будет указывать на какого-то наследника, который что-то мог бы переопределить, потому что у такого класса просто не может быть наследников.
😁 Этот прикол я обнаружил в исходнике Clang.
3️⃣ Мы работаем с объектом класса, а не с указателем на класс. В этом случае точный класс объекта известен на этапе компиляции.
4️⃣ Объект является prvalue. В C++ есть укуренная классификация объектов, где prvalue (pure value) это грубо говоря выражение которое создает новый объект. Смысл в том, что в этом случае тоже известен точный класс объекта на этапе компиляции.
В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редкихпокемона кейса, оба противоречат ООП:
(1) работа с
(2) Класс
Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид.
Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено.
Например, в Apple macOS👩💻 есть Kext (Kernel Extension) - расширения ядра, запускающие то или иное несовместимое с оригинальным маком оборудование. Особенность этих Kext в том, что они могут в рантайме менять vtable, поэтому нельзя делать оптимизации, которые обходят обращение к vtable. В Clang есть флаг -fapple-kext для такой настройки.
А в "обычных" окружениях vtable лежат в секциях наподобии
Теория девиртуализации
Виртуальные функции обычно работают через vtable. Если метод виртуальный, то вместо вызова точно известного метода, в рантайме вычисляется адрес метода, который зависит от динамического типа объекта.
Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный
Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память.
void CallDo(TDerived& obj) {
obj.Do(); // будет ли девиртуализация?
}
Компилятор считает, что можно девиртуализовать вызов в таких случаях:final
.Смысл в том, что даже в случае работы с объектами
TDerived*
/TDerived&
(которые могут указывать на наследника TDerived
) нужный метод будет одним и тем же, как его определил класс TDerived
.struct TDerived : IBase {
void Do() final override; // слово `final` тут
};
struct TDerived final : IBase { // слово final тут
void Do() override;
};
Однако есть еще одно условие, когда класс считается финальным - если у него финальный деструктор struct TDerived : IBase {
~TDerived() final = default; // слово final тут
void Do() override;
};
TDerived derived;
derived.Do(); // это же TDerived, инфа 100%
TDerived MakeDerived(); // просто функция
// ...
MakeDerived().Do(); // здесь будет девиртуализация
TDerived{}.Do(); // здесь тоже девиртуализация
На этом всё! Эта оптимизация логичная и скучная, потому что никаких чудес ожидать не приходится. Смысл в том, чтобы доказать что TDerived
/TDerived&
/TDerived*
указывает именно на объект TDerived
, а не на какой-то его потомок.В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редких
(1) работа с
TDerived*
вместо IBase*
;(2) Класс
TDerived
или нужный метод финальный (не помню когда в последний раз писал final
).Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид.
Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено.
Например, в Apple macOS
А в "обычных" окружениях vtable лежат в секциях наподобии
.rodata
. Эта секция защищена на уровне операционной системы - программа обычно сразу падает при попытке сделать туда какую-нибудь запись в рантайме.Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler
Почему constexpr в компиляторах C++ развивается не в ту сторону🤨
1.5 года назад была написана статья "Дизайн и эволюция constexpr в C++". Там описывается эволюция возможностей constexpr (его вычисление происходит прямо в компиляторе). Потом эту статью перевели на английский PVS-Studio и даже упомянули в твиттере Standard C++😀
Вычисление constexpr-выражений исторически связано с алгоритмом под названием constant folding (wikipedia). Алгоритмы этого рода работают на уровне AST (Abstract Syntax Tree), и изначально были нужны для вычисления простейших выражений с целыми числами. В коде выражение
Вот так обычно вычисляются constexpr-выражения компилятором:
1️⃣ Компилятор сейчас строит AST из исходника на C++.
2️⃣ Встречается выражение, которое нужно вычислить "здесь и сейчас", наподобии такого:
3️⃣ Компилятор вычисляет constexpr-выражение на основе текущего AST, и сразу использует результаты для продолжения построения AST.
4️⃣ Полностью готовый AST переводится в "модуль" LLVM IR (это байткод - промежуточное представление перед переводом в ассемблер).
Проблема в том, что constexpr становится вездесущим и поддерживать его все сложнее и сложнее. Например, до сих пор нет нормального
Хватит это терпеть! Подумал кое-кто, и сейчас делает... всё тот же интерпретатор C++ внутри Clang на основе принципиально нового байткода: ConstantInterpreter (статья от авторов).
Эта штука намного быстрее, чем алгоритмы на AST, но не решает главной проблемы - все равное создается ненужный интерпретатор C++!
Я предложил сделать аутсорс constexpr-вычислений на реальное вычисление на процессоре: тема на форуме clang.
Когда мы собираем "модуль" LLVM IR, мы можем запросить выполнение какого-то кода в формате LLVM IR с использованием данных этого модуля (на процессоре текущего компьютера). Лучше всего это видно на примере этих штук:
1️⃣ Туториал по созданию своего языка программирования с LLVM - раздел 3.5.
2️⃣ Программа lli для выполнения LLVM IR.
3️⃣ clang-repl - вообще балдёжная программа, натуральный интерпретатор C++.
Вот так мог бы вычислять constexpr-выражения компилятор:
1️⃣ Компилятор строит AST, одновременно держится выделенный "модуль" LLVM IR.
2️⃣ Каждое constexpr-выражение вычисляется с использованием этого выделенного "модуля" на процессоре компьютера. В этот модуль пихались бы все данные, нужные этому выражению. В общем, выражение выполняется как в 3️⃣ Полностью готовый AST переводится в новый "модуль" LLVM IR.
И можно выкинуть огромные куски кода для вычисления выражений там на AST.
Конечно, ничего из этого не вышло. Мейнтейнеры компилятора немедленно забросали меня говном за такую харамную идею. Я просто не стал разбирать по частям всё ими написанное, чтобы не вступать в бесполезный спор, настолько безапеляционные были ответы.
Основная мантра с их стороны была про проблемы с
Таким образом, теперь можно понять, почему constexpr в C++ медленно развивается, и возможно никогда не станет полноценным (потому что интерпретировать C++ на AST - плохая идея).
Почему constexpr в компиляторах C++ развивается не в ту сторону
1.5 года назад была написана статья "Дизайн и эволюция constexpr в C++". Там описывается эволюция возможностей constexpr (его вычисление происходит прямо в компиляторе). Потом эту статью перевели на английский PVS-Studio и даже упомянули в твиттере Standard C++
Вычисление constexpr-выражений исторически связано с алгоритмом под названием constant folding (wikipedia). Алгоритмы этого рода работают на уровне AST (Abstract Syntax Tree), и изначально были нужны для вычисления простейших выражений с целыми числами. В коде выражение
4 + 5 * 6
выглядит в AST примерно так:+Поэтому легко написать рекурсивный алгоритм по вычислению этого добра, и потом итеративно добавлять возможности.
├── 4
└── *
├── 5
└── 6
Вот так обычно вычисляются constexpr-выражения компилятором:
template<int N> class Kek { /* ... */ };
// ...
using Kek34 = Kek<4+5*6>; // точное значение аргумента мне запили!
Проблема в том, что constexpr становится вездесущим и поддерживать его все сложнее и сложнее. Например, до сих пор нет нормального
constexpr std::vector<T>
, хотя его должны были сделать еще 4 года назад. Также исправлять баги constexpr реально очень трудно - требуется куча времени, чтобы вникнуть в код. Это своеобразный интерпретатор С++ на AST.Хватит это терпеть! Подумал кое-кто, и сейчас делает... всё тот же интерпретатор C++ внутри Clang на основе принципиально нового байткода: ConstantInterpreter (статья от авторов).
Эта штука намного быстрее, чем алгоритмы на AST, но не решает главной проблемы - все равное создается ненужный интерпретатор C++!
Я предложил сделать аутсорс constexpr-вычислений на реальное вычисление на процессоре: тема на форуме clang.
Когда мы собираем "модуль" LLVM IR, мы можем запросить выполнение какого-то кода в формате LLVM IR с использованием данных этого модуля (на процессоре текущего компьютера). Лучше всего это видно на примере этих штук:
Вот так мог бы вычислять constexpr-выражения компилятор:
clang-repl
И можно выкинуть огромные куски кода для вычисления выражений там на AST.
Конечно, ничего из этого не вышло. Мейнтейнеры компилятора немедленно забросали меня говном за такую харамную идею. Я просто не стал разбирать по частям всё ими написанное, чтобы не вступать в бесполезный спор, настолько безапеляционные были ответы.
Основная мантра с их стороны была про проблемы с
cross-compilation
(когда программу собирают под другую платформу). Видимо, есть на свете такие платформы, что там 4+5*6
равняется не не 34
, а чему-то еще.Таким образом, теперь можно понять, почему constexpr в C++ медленно развивается, и возможно никогда не станет полноценным (потому что интерпретировать C++ на AST - плохая идея).
Please open Telegram to view this post
VIEW IN TELEGRAM
#books
Обзор книги "Linux Kernel Development" (2010 г.) 📚👩💻
(можно скачать PDF тут, лучше читать оригинал на английском, а не перевод Гоблина)
Автор этой книги писал код для ядра Linux на протяжении 15 лет (на момент написания книги) в качестве основной работы и сделал там много хорошего.
В этой книге на 400+ страниц содержится информация, нужная для начала разработки кода в ядре Линукса, по состоянию на релиз 2.6.34. Объем информации большой - ведь во многих open source проектах достаточно вникать в код всего несколько дней или часов, чтобы туда что-то написать.
В книге описание "как отправить патч" есть только в одной главе в конце, а в остальном информация выглядит так:
1️⃣ Старые песни о главном: как работает
2️⃣ Копипаст сишной структуры из исходного кода и описание каждого его поля.
3️⃣ Описание разнообразных настроек, которые могут повлиять на каждую сферу работы ядра.
4️⃣ Иногда описание используемой структуры данных (например красно-черных деревьев)
Ядро написано на ISO C99 и активно используеткостыли GNU C Extensions. Я отметил самые крутые на мой взгляд особенности разработки ядра, о которых не задумываются в "обычных" проектах:
⭕️ Ядро не использует стандартную библиотеку C. Нужные функции просто скопипасчены. Например, вместо
⭕️ Почти нет операций с float, потому что для этого требуется ручная работа с float-регистрами процессора.
⭕️ Стек ядра (kernel stack) жестко ограничен - от 4KB до 16KB.
⭕️ Самый часто используемый инструмент синхронизации - спинлок, с "активным" ожиданием разблокировки в while-цикле. Он обусловлен тем, что в ядре всё происходит сравнительно быстро и дожидаться разблокировки в цикле выходит "дешевле", чем делать всю байду с укладыванием процесса в "сон" и его "пробуждение".
⭕️ Для аллокации страничной памяти есть
⭕️ Часто используются "аллокаторы маленьких объектов". Например,
⭕️ Ядро нельзя нормально дебажить, все пишут логи, чтобы понять что происходит. 😈
После этой книги я понял, насколько Linux огромен. Можно сказать, что эта книга "обо всем и ни о чем". Если можно сказать, что компилятор для C++ это "четкая цель", то ядро Linux это "нечеткая цель" - его можно собрать в миллиарде разных конфигураций.
Например: В описании обработчиков прерываний (interrupt handlers - грубо говоря обработка событий типа "ввод с клавиатуры") мы узнаем, что обработчик должен быть мега-быстрым, потому что пока обрабатывается одно прерывание, все остальные прерывания идут лесом.
Поэтому обработка прерывания разделена на две части -
Так вот, для обработки части😁
И так во всем - везде будет огромное количество опций.
По состоянию на 2010 год официально поддерживалось 60 файловых систем.
Есть целые классы алгоритмов для планировщика процессов.
Несколько методов управления I/O Scheduling (ввод-вывод с жестким диском).
Есть море драйверов, которые можно вкомпилировать в образ ядра по желанию.
Полностью "выучить" Linux невозможно, по каждой части можно писать отдельную книгу📝
Обзор книги "Linux Kernel Development" (2010 г.) 📚
(можно скачать PDF тут, лучше читать оригинал на английском, а не перевод Гоблина)
Автор этой книги писал код для ядра Linux на протяжении 15 лет (на момент написания книги) в качестве основной работы и сделал там много хорошего.
В этой книге на 400+ страниц содержится информация, нужная для начала разработки кода в ядре Линукса, по состоянию на релиз 2.6.34. Объем информации большой - ведь во многих open source проектах достаточно вникать в код всего несколько дней или часов, чтобы туда что-то написать.
В книге описание "как отправить патч" есть только в одной главе в конце, а в остальном информация выглядит так:
fork()
, виртуальная страничная память, прерывания, планировщик задач, syscalls, драйвера, и т.д. и т.п.Ядро написано на ISO C99 и активно использует
printf
используется функция printk
. Еще скопипасчены строковые алгоритмы.vmalloc
- аллоцирует виртуально непрерывную память, и kmalloc
- аллоцирует физически непрерывную память. "Обычные" программы практически всегда используют vmalloc
, но ядро практически всегда использует kmalloc
для быстроты, чтобы не возиться со структурами виртуальной памяти. Тред на stackoverflowslab allocator
очень напомнил мне блоковый аллокатор из Box2D (хотя я не большой специалист в аллокаторах).gdb
не работает нормально, он не может никак модифицировать данные ядра, ставить breakpoint-ы и выполнять код step by step. В книге приводится какая-то укуренная схема с костылем kgdb
, где используется два компьютера, и один компьютер дебажит ядро второго через шнур. После этой книги я понял, насколько Linux огромен. Можно сказать, что эта книга "обо всем и ни о чем". Если можно сказать, что компилятор для C++ это "четкая цель", то ядро Linux это "нечеткая цель" - его можно собрать в миллиарде разных конфигураций.
Например: В описании обработчиков прерываний (interrupt handlers - грубо говоря обработка событий типа "ввод с клавиатуры") мы узнаем, что обработчик должен быть мега-быстрым, потому что пока обрабатывается одно прерывание, все остальные прерывания идут лесом.
Поэтому обработка прерывания разделена на две части -
"top half"
в interrupt handler (очень быстро сделать вещи, скажем поставить флаг где-то в ядре), и "bottom half"
когда-то потом (копировать данные, менять структуры в ядре, блокировать поток и тд и тп).Так вот, для обработки части
"bottom half"
по состоянию на 2010 год было 5 механизмов - 2 устаревших и 3 активных И так во всем - везде будет огромное количество опций.
По состоянию на 2010 год официально поддерживалось 60 файловых систем.
Есть целые классы алгоритмов для планировщика процессов.
Несколько методов управления I/O Scheduling (ввод-вывод с жестким диском).
Есть море драйверов, которые можно вкомпилировать в образ ядра по желанию.
Полностью "выучить" Linux невозможно, по каждой части можно писать отдельную книгу
Please open Telegram to view this post
VIEW IN TELEGRAM
#story
Как я решал задачи по CTF😀
CTF (Capture The Flag) это соревнования, где участники решают веселые задачки "типа" по кибербезопасности.Забегая вперед, я не понял при чем тут кибербезопасность.
Пару лет назад я порешал такие задачи, чтобы понять что это такое. Мне понравился сайт ctflearn.com (мой профиль). За несколько вечеров можно порешать несколько десятков задач.
Задачи разделяются на несколько категорий.
Дается какая-то сущность (адрес сайта, архив, бинарник, изображение) и из этой сущности надо вытащить "флаг" - строку наподобии
На мой взгляд, умение решать такие задачи никуда не конвертируется. Это очень сильно "вещь в себе". Рекламируется, что подобные задачи сделаны типа для "хакеров", но это жесткий развод, потому что по-моему хакеры не этим занимаются.
Кроме того, многие задачи никогда не догадаешься как решать, если не решал что-то подобное раньше (с подсказками), поэтому прямо на смекалочку задач не так много.
Какие типичные задачи попадаются и как их решать?
💻 Дается изображение.
Некоторые форматы изображений поддерживают "комментарии" (и прочие метаданные), находим флаг там. "Комментарий" можно увидеть в GIMP или других редакторах (открыв там картинку), но проще всего запустить команду в терминале.
💻 Дается бинарник.
При запуске
💻 Дается бинарник.
По дизассемблеру видим, что требуется "типа" ввести какую-то правильную строку, чтобы показать флаг.
Вообще если где-то в коде есть
Решением является заменить руками несколько байт в бинарнике на инструкцию
💻 Дается бинарник.
Теперь флаг не выводится сам, а просто есть намертво обфусцированные функции. Предлагается вручную восстановить "на бумаге" алгоритм в этих функциях. Такие задачи можно решать до трёх часов (у меня было так), сидя в
Видел, что иногда бывает дизассемблер Python или Java, по идее это должно быть в 100500 раз проще C/C++, но мне такие задачи не встречались.
💻 Дается база данных.
Нужно написать запрос, который покажет ключ. Обычно делается в виде SQL-инъекции, типа вводишь
Однако бывают ублюдские задачи, например в одной из них надо было по-моему знать специфические команды MySQL (которых ни в какой другой СУБД нет), которые покажут все доступные таблицы, и вывести данные из "тайной" таблицы.
💻 Дается изображение.
Теперь флаг реально зашифрован в изображении, а не в его метаданных. Это называется "стеганография".
Часто оказывается, что флаг можно увидеть, скажем посмотрев только зеленую маску (из пикселей
Если это картинка типа "помехи телевизора", можно отзеркалить картинку и наложить на исходную, тогда наложение покажет ключ.
Есть целые тулзы для классов задач(!) Для стеганографии это zsteg.
💻 Дается изображение или другой файл.
Часто нужный файл/файлы "спрятан" сразу после первого. То есть грубо говоря взяли файл картинки и сразу после последнего байта картинки приписали файл архива.
Прикол в том, что просмотрщик картинки игнорирует "лишние" байты, и обнаружить спрятанный архив можно только из терминала.
Это детектится по сигнатурам - например архив начинается с человекочитаемых байтов
💻 Дается звуковый файл.
Обычно его даже не прослушивают😁 А сразу смотрят на спектрограмму файла. Там может находиться флаг или какой-нибудь QR-код.
(продолжение в комментарии к посту, потому что у Телеграма есть ограничение по размеру😤 )
Как я решал задачи по CTF
CTF (Capture The Flag) это соревнования, где участники решают веселые задачки "типа" по кибербезопасности.
Задачи разделяются на несколько категорий.
Дается какая-то сущность (адрес сайта, архив, бинарник, изображение) и из этой сущности надо вытащить "флаг" - строку наподобии
flag{w0w_y0u_ar3_c00l_h@cker}
, которая может находиться в неожиданных местах.На мой взгляд, умение решать такие задачи никуда не конвертируется. Это очень сильно "вещь в себе". Рекламируется, что подобные задачи сделаны типа для "хакеров", но это жесткий развод, потому что по-моему хакеры не этим занимаются.
Кроме того, многие задачи никогда не догадаешься как решать, если не решал что-то подобное раньше (с подсказками), поэтому прямо на смекалочку задач не так много.
Какие типичные задачи попадаются и как их решать?
Некоторые форматы изображений поддерживают "комментарии" (и прочие метаданные), находим флаг там. "Комментарий" можно увидеть в GIMP или других редакторах (открыв там картинку), но проще всего запустить команду в терминале.
При запуске
./hack_me
выводит хреновню. Однако где-то в нем в открытом виде (не обфусцированном) спрятан флаг. Запускаем strings hack_me
(оно ищет человекочитаемые строки) и видим флаг.По дизассемблеру видим, что требуется "типа" ввести какую-то правильную строку, чтобы показать флаг.
Вообще если где-то в коде есть
const char* f = "flag{...}";
то этот flag{...}
попадает в бинарь в виде человекочитаемой строки, однако авторы просто обфусцируют этот флаг, чтобы он вычислялся по переусложненной схеме.Решением является заменить руками несколько байт в бинарнике на инструкцию
jmp
до вывода флага (чтобы он вывелся без условий).Теперь флаг не выводится сам, а просто есть намертво обфусцированные функции. Предлагается вручную восстановить "на бумаге" алгоритм в этих функциях. Такие задачи можно решать до трёх часов (у меня было так), сидя в
gdb
и дизассемблере.Видел, что иногда бывает дизассемблер Python или Java, по идее это должно быть в 100500 раз проще C/C++, но мне такие задачи не встречались.
Нужно написать запрос, который покажет ключ. Обычно делается в виде SQL-инъекции, типа вводишь
1" OR «1» = «1»
и получаешь все записи. Напоминаю, что это никак не относится к хакерству, потому что даже самые тупые веб-фреймворки умеют обезопасиваться от этих школьных приемов.Однако бывают ублюдские задачи, например в одной из них надо было по-моему знать специфические команды MySQL (которых ни в какой другой СУБД нет), которые покажут все доступные таблицы, и вывести данные из "тайной" таблицы.
Теперь флаг реально зашифрован в изображении, а не в его метаданных. Это называется "стеганография".
Часто оказывается, что флаг можно увидеть, скажем посмотрев только зеленую маску (из пикселей
(r,g,b)
сделать (0,g,0)
).Если это картинка типа "помехи телевизора", можно отзеркалить картинку и наложить на исходную, тогда наложение покажет ключ.
Есть целые тулзы для классов задач(!) Для стеганографии это zsteg.
Часто нужный файл/файлы "спрятан" сразу после первого. То есть грубо говоря взяли файл картинки и сразу после последнего байта картинки приписали файл архива.
Прикол в том, что просмотрщик картинки игнорирует "лишние" байты, и обнаружить спрятанный архив можно только из терминала.
Это детектится по сигнатурам - например архив начинается с человекочитаемых байтов
Rar!
. По-моему тоже есть тулза специально для CTF, которая детектит такие файлы.Обычно его даже не прослушивают
(продолжение в комментарии к посту, потому что у Телеграма есть ограничение по размеру
Please open Telegram to view this post
VIEW IN TELEGRAM
#madskillz
Напиши свой собственный RTTI🖱
RTTI (run-time type information) это некая информация о всех виртуальных классах, которая попадает в бинарник программы.
Благодаря этому работают dynamic_cast<> (приведение типа в run-time) и typeid.
Генерацию RTTI можно выключить флагом компиляции
Пусть у нас есть указатель
Есть три варианта приведений типа
1️⃣ Upcast:
Для этого не требуется никакого RTTI. Такое приведение всегда возможно. Можно было бы обойтись static_cast<> (приведение типа в compile-time).
2️⃣ Downcast:
В этом случае обычно используют
Интересно, что если программист совершенно уверен, что приведение возможно, то можно использовать😁
3️⃣ Sidecast:
Такое возможно, если
Скорее всего sidecast значит, что в программе есть серьезные ошибки дизайна🧐
Некоторые проекты не используют "официальный" RTTI😡
Почему так происходит, на примере очень популярных проектов:
1️⃣ Protobuf: вынужденная поддержка проектов с
В protobuf есть базовый для всех "месседжей" класс
Чтобы попробовать сделать downcast от базового класса до класса "месседжа", можно использовать функцию DynamicCastToGenerated.
Как видно по исходнику, если уж нельзя вызвать
Какие ограничения этого подхода: С
2️⃣ LLVM: своя реализация RTTI для быстродействия.
Clang и LLVM имеют большую иерархию типов и делают огромную кучу проверок на типы. Большая часть кода в компиляторах (оптимизации, кодогенерация, ...) завязана на поиск специфических паттернов и операциях на них. Для этого необходимо проверять тип объектов в овер9000 местах, поэтому быстродействие
А быстродействие у
В документации есть крутая статья, как сделать свой RTTI "почти как в LLVM" - How to set up LLVM-style RTTI for your class hierarchy. Для этого заводится специальный
Какие ограничения этого подхода: Дополнительный код -
Всего известно три главных аргумента "против RTTI":
🅱️ Занимает память в бинарнике - очень сомнительный аргумент. RTTI не требует столько памяти, чтобы это стало заметно, по крайней мере в 2023 году.
🅱️ Медленно работает - хороший аргумент, если
🅱️ Его использование - ошибка дизайна - неожиданный аргумент, но именно по этой причине RTTI запрещен в Google C++ Style Guide. По ссылке есть описание "почему это плохо". Конечно, из каждого правила есть исключение.
Напиши свой собственный RTTI
RTTI (run-time type information) это некая информация о всех виртуальных классах, которая попадает в бинарник программы.
Благодаря этому работают dynamic_cast<> (приведение типа в run-time) и typeid.
Генерацию RTTI можно выключить флагом компиляции
-fno-rtti
.Пусть у нас есть указатель
X* x
(или ссылка X& x
). Указатель может указывать на объект с типом не X
(но это обязательно будет тип-потомок X
).Есть три варианта приведений типа
X
к типу Y
(тип Y
тоже не обязательно реальный тип объекта):Y
- класс-предок X
.Для этого не требуется никакого RTTI. Такое приведение всегда возможно. Можно было бы обойтись static_cast<> (приведение типа в compile-time).
Y
- класс-потомок X
.В этом случае обычно используют
dynamic_cast<>
. Если окажется, что тип объекта не Y
(или не какого-то потомка Y
), то приведения не случится.Интересно, что если программист совершенно уверен, что приведение возможно, то можно использовать
static_cast<>
и не делать run-time проверку. Если окажется, что он был не прав, то получится undefined behaviour X
и Y
никак не связаны между собой.Такое возможно, если
X*
указывает на объект класса Z
:class Z : public X, public Y {...};Такие касты
dynamic_cast<>
тоже умеет делать.Скорее всего sidecast значит, что в программе есть серьезные ошибки дизайна
Некоторые проекты не используют "официальный" RTTI
Почему так происходит, на примере очень популярных проектов:
-fno-rtti
.В protobuf есть базовый для всех "месседжей" класс
google::protobuf::Message
.Чтобы попробовать сделать downcast от базового класса до класса "месседжа", можно использовать функцию DynamicCastToGenerated.
Как видно по исходнику, если уж нельзя вызвать
dynamic_cast<>
, то используется костыль - суррогат RTTI: сравнение ссылки на "рефлексию" (уникальное описание "месседжа"). Эту ссылку возвращает виртуальный метод.Какие ограничения этого подхода: С
-fno-rtti
доступен только downcast строго на указанный класс - потомок класса Message
.Clang и LLVM имеют большую иерархию типов и делают огромную кучу проверок на типы. Большая часть кода в компиляторах (оптимизации, кодогенерация, ...) завязана на поиск специфических паттернов и операциях на них. Для этого необходимо проверять тип объектов в овер9000 местах, поэтому быстродействие
dynamic_cast
становится узким местом.А быстродействие у
dynamic_cast
сравнительно плохое. Он должен делать обход иерархии наследования и вычисляет путь обхода динамическим образом. Это на несколько порядков медленнее, чем просто вызвать виртуальный метод и что-то сравнить.В документации есть крутая статья, как сделать свой RTTI "почти как в LLVM" - How to set up LLVM-style RTTI for your class hierarchy. Для этого заводится специальный
enum
, и каждый класс реализует статический метод classof
. Вместо обхода иерархии наследования делается один вызов виртуального метода!Какие ограничения этого подхода: Дополнительный код -
enum
, статический метод в каждом классе. Нужно следить за тем, чтобы соответствие между enum
и классами не разломалось (хотя тут могут помочь кодогенераторы). Эта схема работает, только если иерархия классов известна заранее (стандартный C++ RTTI такого не требует).Всего известно три главных аргумента "против RTTI":
dynamic_cast<>
является узким местом в программе. Но это должна быть специфическая программа, как поиск паттернов в структурах с большой иерархией классов... (например, компилятор C++)Please open Telegram to view this post
VIEW IN TELEGRAM