Pull to refresh
313
0
Николай Шлей@CodeRush

Firmware Security Engineer

Send message
От нестандартности всё. Этих #pragma развели три мешка, даже в стандарт добавили в #pragma STDC, а большую часть проблем они если и решают, то только на определенных компиляторах и при определенной фазе луны. От того их и не любят почти все, кто с ними работает.
Внезапно, отлично, буду знать.
У меня, честно говоря, нет ни malloc, ни free (вместо них AllocatePool/FreePool и AllocatePages/FreePages, первая из которых по умолчанию выравнивает по sizeof(size_t), а вторая — по размеру страницы), но я помню проблемы с выравниванием выделенной malloc памяти при работе с Cortex-M0 в Keil uVision 4, видимо, это был баг в реализации для этих ядер или просто процессор попался неудачный.
Вот про «может быть и больше» я и пытаюсь говорить, но видимо, построил слишком уж категорический императив, прошу пардону.
Сигнатуры — видел:
void* memcpu(void*, const void*, size_t)
void* malloc(size_t)

Теперь поговорим о байтах. Если вы считаете, что «байт» — это uint8_t, то в общем случае, вы не правы. Если считаете, что malloc выделяет ровно столько «байт», сколько вами запрошено, то вы тоже не всегда правы (это сильно зависит от реализации, может вообще целую страницу выделить).
Все, что вам гарантирует malloc — это то, что вам вернут достаточно большой кусок нетипизированной памяти для хранения указанного числа «байт», размер которых зависит от архитектуры, и что память потом можно будет освободить при помощи free, либо NULL, если выделить не получилось. Обычная malloc из C даже выравнивание не гарантирует, и в POSIX специально пришлось вводить posix_memalign, чтобы не ловить CPU Exception'ы в случайном месте.
Все, что я хочу тут сказать — в языке нет и не было понятия «восьмибитный байт», а само слово «байт» в документации используется в значении «минимальная единица размера типа».
Еще раз, в C никаких байт нет, и потому никакая память в них не измеряется, точка, конец истории.
Если делать функции работы с памятью через uint8_t*, то на некоторых архитектурах с hard-fail на misaligned read, к примеру, на ARM Cortex-M0, у вас процессор будет просто зависать на первом же обращении к такой памяти примерно в 3/4 случаев.
void* — это не «указатель на ничего», это указатель на кусок памяти без типа, ровно такой, как нам её выделяет malloc() и освобождает free(). В C, вообще говоря, нет типа данных «байт», и даже нигде не написано, что в байте должно быть 8 бит (это написано в стандарте POSIX, но на C можно писать и для архитектур с другим размером байта), и, таким образом, использовать uint8_t в этом месте — совершенно неразумное ограничение как переносимости, как так и удобства использования вашего кода. Понятно, что явное лучше, чем неявное, но в данном случае void* все-таки намного лучше отражает намерения программиста, на мой взгляд.
Переводите оба тогда, там есть что обсудить и о чем поспорить.
Там не только с маскированием ошибок проблемы, там integer overflow потенциальный при любом вызове. Если уж советовать замену, то на reallocarray() из OpenBSD, что автор вышеупомянутой статьи и предлагает.
Очень много крайне спорных советов, на самом деле. Вот достойный ответ, особенно на использование calloc.
Ради школьников тратить §20M на RnD и $10M на отладку получившейся «аппаратной поддержки второй стрелки» никто не будет, извините. Пусть эти школьники спокойно пьют свое пиво.
Не могу не процитировать часть вот этого ответа на SO, который однозначно стоит прочитать целиком:
What must be understood is that in all these constructions, each circuit gate must map to an instance of Gentry's fully homomorphic encryption scheme, and at every clock cycle for the obfuscated circuit, all gates must be processed, regardless of whether they are «active» or not in the circuit (this is a big part of why the obfuscation theoretically works: it does not reveal active gates, by always making them all active). This article gives performance result: on a rather big PC, we are up for minutes of computation. That's for each gate in the obfuscated circuit, and for each clock cycle.
В общем, насколько я понял из статей и процитированного ответа, там вся теоретическая обфускация разбивается о практическую производительность, и с этим за пару лет ничего сделать не получится.
Всем, libstdcpp и STLPort — два разных рантайма с разными же возможностями (и размерами), первый предосталяет только некоторые функции, а второй — всю стандартную библиотеку.
Тем не менее, даже гугловская libstdc++ не поддерживает STL, так что в каком-то смысле они тоже правы.
Катит, но там не только их не хватает, а в итоге нет ни исключений, ни стандартной библиотеки, и непонятно, зачем вообще огород городить, если на C уже все готово. В Rust хотя бы borrow checker поможет и иммутабельность по умолчанию, а чем хорош C++ в данном случае я просто не знаю.
Не получится, т.к. нет рантайма (т.е new, delete и прочие красивости C++ просто не работают), а без него C++ мало чем лучше C. Вот тут Count Chu рассказывает, как он заводил код на C++ в EDK2, но успехом я такое решение точно не назову.
Уже однажды попробовал и не смог.
Среда UEFI достаточно сильно отличается от привычной стандартной библиотеки, к примеру, там почти все строки — в UCS2, и уже одно это приносит кучу боли, т.к. строковые литералы просто как L«String» теперь не запишешь — в Rust UTF8 везде. Плюс абсолютно всё получается внутри unsafe {}, и в итоге преимуществ у Rust почти не остается, а недостатки вроде необходимости писать врапперы к библиотекам и протоколам — никуда не деваются. Я видел пару попыток, но их авторы тоже бросили, практически не начав.
Если время найду, попробую еще раз.
Мне он не нравится тем, что я вынужден проходить цепочку из макросов и typedef'ов, чтобы понять, что это за зверь вообще и как он будет выглядеть из окна аппаратного отладчика. Т.е. одно дело, когда на тебя из этого окна смотрит целая переменная со значением 17, и совсем другое — когда это не целая переменная, а указатель на целую, или на функцию, что еще хуже.
Сам по себе typedef — это благо, а проблемы такого рода решаются грамотными IDE, но они от этого не перестают быть проблемами.
По мне тоже, но случается и вот такое, причем у всех, даже у самых гурушных гуру. У нас там все проще в UEFI — прошивка отрабатывает быстро и умирает быстро, а всю память, выделенную как EfiBootServicesCode или EfiBootServicesData освободит менеджер BDS, просто пометив ее свободной в UEFI MemMap, и ее потом перепишет ОС, когда ей понадобится. Т.к. мало кто в прошивке пытается выделить себе с кучи гигабайт-другой, то вся система довольно спокойно отностится к утечкам — в конце все равно останутся только пара рантайм-сервисов, для которых управление памятью уже написано и работает нормально.
Я тут рассматриваю не только язык сам по себе, но и все, что вокруг, поэтому директивы компилятора, разные #pragma и прочие __attribute__((declspec)) я тоже отношу к C, просто не к чему больше. Не спорю, опять же, что стандартизировать такое сложно, однако #pragma STDC ... как-то пробрались в C99, значит не все еще потеряно в этом смысле.

По поводу отношения соглашения о вызовах к указателям на функцию, простой пример: все компоненты UEFI находятся в одном адресном пространстве и общаются путем прямого вызова функций через таблицы указателей на них (такие таблицы называются «протоколами»). При этом, для архитектуры amd64 существует два распространенных соглашения о вызовах: Microsoft x64 (которое и используется в 64-битном UEFI и которое использует по умолчанию компилятор MSVC) и System V amd64 ABI, которое используют
*nix-системы и которое по умолчанию использует GCC. При этом компоненты UEFI не собираются одним и тем же компилятором, и зачастую поставляются в бинарном виде, поэтому очень важно, чтобы соглашение о вызовах было одинаковое, а это достигается добавлением EFIAPI ко всем typedef'ам всех указателей на функции, являющиеся частью протоколов, к прототипам и реализациям этих функций.
Пример кода
#include "Uefi.h"

// Define SIO DXE protocol GUID
#define EFI_CRSIO_DXE_PROTOCOL_GUID \
{ 0xc38bdbd, 0x6f6b, 0x49ae, 0x99, 0x5a, 0x1d, 0x6c, 0xc2, 0x9d, 0x95, 0xa1 }

// Extern the GUID for protocol users.
extern EFI_GUID gEfiCrSioDxeProtocolGuid;

// Forward reference for ANSI C compatibility
typedef struct _EFI_CRSIO_DXE_PROTOCOL EFI_CRSIO_DXE_PROTOCOL;

// Define each protocol interface function and the respective function pointer type 
typedef
UINT16
(EFIAPI *CRSIO_DXE_GET_INDEX) ( //Если вот здесь забыть EFIAPI, ничего не сломается, т.к. функция не имеет параметров. 
    VOID                        //А вот если бы они были, а компилятором оказался GCC, то он бы их положил в RDI, RSI...                         
);                              //а функция их ждет в RCX, RDX..., т.е. на вид все хорошо, а по факту в регистрах мусор вместо параметров

UINT16
EFIAPI // А если забыть его вот здесь, не забыв выше, будет почти то же самое, только упадет все в другом месте
CrSioGetIndex(
    VOID
);

// Define protocol data structure
typedef struct _EFI_CRSIO_DXE_PROTOCOL 
{
    CRSIO_DXE_GET_INDEX CrSioGetIndex;
} EFI_CRSIO_DXE_PROTOCOL;

Проблема то решена, но какой-то осадок неприятный все равно остается, т.к. четверть кода, который я пишу — макросы, обернутые в макросы, обернутые в typedef'ы, но альтернативы пока нет, спасибо, что не на ассемблере.
А что делать с обработкой ошибок, копировать все вызовы free() в каждый if (EFI_ERROR(Status)) или использовать goto fail, или может быть do { if (error_condition) break; } while(0), если у вас на goto аллергия?
Если бы простыми правилами можно было бы обойтись, память не текла бы у практически всех программ на С сложнее hello.c. Язык не виноват, но люди устроены именно так.

Information

Rating
Does not participate
Date of birth
Registered
Activity

Specialization

Инженер встраиваемых систем, Системный инженер
Ведущий