Почему адрес 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 вообще не возникают из-за доступа к памяти (например, попытки чтения или записи регистров управления из непривилегированного режима), и в этом случае нет адреса сбоя.