Почему адрес segfault имеет значение NULL при доступе к памяти, в которой установлен любой из 16 наиболее значимых битов?

Рассмотрим следующую программу сборки:

bits 64
global _start
_start:
    mov rax, 0x0000111111111111
    add byte [rax*1+0x0], al
    jmp _start

Когда вы компилируете это с помощью nasm и ld (в Ubuntu, ядро ​​5.4.0-48-generic, Ryzen 3900X), вы получите ошибку segfault:

$ ./segfault-addr
[1]    107116 segmentation fault (core dumped)  ./segfault-addr

Когда вы прикрепляете gdbвы можете увидеть адрес, вызвавший эту ошибку:

(gdb) p $_siginfo._sifields._sigfault.si_addr
$1 = (void *) 0x111111111111

Однако, если вы установите для любого из 16 наиболее значимых битов значение 1 следующим образом:

bits 64
global _start
_start:
    mov rax, 0x0001111111111111
    add byte [rax*1+0x0], al
    jmp _start

Очевидно, что вы все еще получаете segfault, но теперь адрес NULL:

(gdb) p $_siginfo._sifields._sigfault.si_addr
$1 = (void *) 0x0

Почему это происходит? Это вызвано gdb, Линукс или сам процессор?

Что я могу сделать, чтобы предотвратить такое поведение?

1 ответ

Решение

Разница между каноническими и неканоническими адресами заключается в том, что x86-64 не имеет полного 64-битного виртуального адресного пространства. Ваш второй пример - это неканонический адрес, так как это не 48-битное значение с расширенным знаком (очевидно, на вашем компьютере нет пятиуровневого расширения таблицы страниц, иначе оно будет 57 бит); такие адреса никогда не могут быть преобразованы в физическую память.

Недопустимые обращения к каноническим адресам генерируют ошибку страницы (#PF), для которой ЦП предоставляет ядру адрес с ошибкой (в регистре CR2), и ядро ​​передает его в пользовательское пространство в si_addr поле struct siginfoкак вы видите. Но доступ к неканоническим адресам всегда недействителен, и ЦП вызывает общее исключение защиты (#GP) или, в редких случаях, сбой стека (#SS). Разработчики архитектуры x86 в своей безграничной мудрости решили не предоставлять программному обеспечению адрес сбоя в случае исключения #GP или #SS, чтобы ядро ​​не получило его, и вы тоже.

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


Я предполагаю, что это решение было вызвано тем, что ядру действительно нужен адрес в случае ошибки страницы. Доступ к отсутствующей странице может быть нарушением памяти, которое должно убить процесс; или, например, это может быть просто страница, выгруженная из физической памяти. В последнем случае ядро ​​использует адрес ошибки, чтобы найти соответствующую страницу на диске и загрузить ее обратно в физическую память. Затем он обновляет таблицы страниц и возвращается из обработчика исключений, чтобы перезапустить сбойную инструкцию, и программа может продолжить работу.

Однако общий сбой защиты, как правило, не подлежит устранению, и процесс должен быть остановлен или, по крайней мере, сигнализирован, чтобы он мог попытаться очистить. В этом случае с ошибочным адресом ничего нельзя сделать, и я предполагаю, что разработчики архитектуры не думали, что его потенциальное значение для отладки стоило усилий по сохранению ЦП. В любом случае, многие возможные причины #GP вообще не возникают из-за доступа к памяти (например, попытки чтения или записи регистров управления из непривилегированного режима), и в этом случае нет адреса сбоя.

Другие вопросы по тегам