Недавно я написал следующий глючный код на C:
#include <stdio.h>
struct IpAddr {
unsigned char a, b, c, d;
};
struct IpAddr ipv4_from_str (const char * str) {
struct IpAddr res = {0};
sscanf(str, "%d.%d.%d.%d", &res.a, &res.b, &res.c, &res.d);
return res;
}
int main (int argc, char ** argv) {
struct IpAddr ip = ipv4_from_str("192.168.1.1");
printf("Read in ip: %d.%d.%d.%d\n", ip.a, ip.b, ip.c, ip.d);
return 0;
}
Ошибка в том, что я использую %d
в sscanf
, предоставляя указатели на беззнаковый символ шириной 1 байт. %d
принимает указатель int шириной 4 байта, и разница приводит к записи за пределами границ. out-of-bound запись однозначно ub, и программа рухнет.
Меня смущает непостоянный характер ошибки. После 1000 запусков программа выдает ошибку сегментации до в операторе печати в 50% случаев, а сегментацию ошибки после в остальных 50% случаев. Я не понимаю, почему это может измениться. В чем разница между двумя вызовами программы? У меня сложилось впечатление, что расположение памяти в стеке согласовано, и небольшие тестовые программы, которые я написал, похоже, подтверждают это. Разве это не так?
Я использую gcc v11.3.0 на Debian bookworm, ядро 5.14.16-1, и я скомпилировал без каких-либо установленных флагов.
Здесь — это вывод сборки моего компилятора для справки.
@ Бармар, вот и все. Отключение ASLR вызывает регулярные сбои. Думаю, я неправильно понял, что на самом деле делает ASLR. Если вы опубликуете ответ, я отмечу его как правильный, в противном случае я напишу объяснение и укажу вам.
Ваша функция ipv4_from_str
перезаписывает младшие 3 байта сохраненного указателя кадра rbp
, поэтому по возвращении rbp
поврежден. Если он указывает на неверный адрес, произойдет сбой на следующей же инструкции movl %eax, -4(%rbp)
. Если он указывает на действительный адрес, вы продолжаете использовать мусорные значения, но leave
в конце main
перемещает rbp
в rsp
. Затем адрес возврата извлекается из некоторого случайного места в памяти и, скорее всего, указывает на то, что не является исполняемым кодом (ноль в моем тестовом прогоне), поэтому в этот момент происходит сбой.
Неопределенное поведение означает, что может произойти что угодно, даже противоречивые результаты.
На практике это несоответствие, скорее всего, связано с Рандомизация макета адресного пространства. В зависимости от того, как данные расположены в памяти, доступ за пределы может обращаться или не обращаться к нераспределенной памяти или перезаписывать критический указатель.
См. также Почему я не получаю ошибку сегментации, когда пишу за конец массива?
Рандомизация макета адресного пространства: techtarget.com/searchsecurity/definition/…