Уже довольно долгое время ядро Linux имеет опцию записи в журнал сообщения ядра о каждой сбойной пользовательской программе, и, вероятно, в вашем дистрибутиве Linux она по умолчанию включена.
Пример
1 | testp[1234]: segfault at 0 ip 000000000000401272 sp 00007fff2ce4d230 error 4 in testp[300000+98000] |
Значение этого следующее:
- 'testp[1234]' - это неисправная программа и ее PID.
- 'segfault at 0' - говорит нам адрес памяти (в шестнадцатеричном формате), который вызвал ошибку, когда программа пыталась получить к нему доступ. В данном случае адрес равен 0, так что мы имеем какое-то нулевое разыменование.
- 'ip 0000000000401272' - это значение указателя инструкции во время ошибки. Это должна быть инструкция, которая пыталась выполнить некорректный доступ к памяти. В 64-битном x86 это будет регистр %rip (полезно для проверки в GDB и других местах).
- 'sp 00007fff2ce4d230' - значение указателя стека. В 64-битном x86 это будет %rsp.
- 'error 4' - это биты кода ошибки сбойной страницы из traps.h в шестнадцатеричном формате, как обычно, и почти всегда будет не менее 4 (что означает "доступ в пользовательском режиме"). Значение 4 означает, что это было чтение неразмеченной области, например, адреса 0, а значение 6 (4+2) означает, что это была запись неразмеченной области.
- 'in testp[300000+98000]' говорит нам о конкретной области виртуальной памяти, в которой находится указатель инструкции, указывая, какой это файл (здесь это исполняемый файл), начальный адрес, по которому отображается VMA (0x300000), и размер отображения (0x98000).
С адресом ошибки 0 и кодом ошибки 4 мы знаем, что эта конкретная ошибка является чтением нулевого указателя.
Вот еще два сообщения об ошибках:
1 | bash[1234]: segfault at 1054807 ip 000000000041d989 sp 00007ffec1f1cbd8 error 6 in bash[400000+f3000] |
'Error 6' означает запись на не отображенный пользовательский адрес, здесь 0x1054807.
1 | bash[1234]: segfault at 0 ip 00007f83c02db746 sp 00007ffcfbeda010 error 4 in libc-2.23.so[7f82c0350000+1c0000] |
Ошибка 4 и адрес 0 - это чтение нулевого указателя, но на этот раз в какой-то функции libc, а не в собственном коде bash, поскольку об этом сообщается как 'in libc-2.23.so'.
В 64-битном x86 Linux вы получите несколько иное сообщение, если проблема на самом деле в выполняемой инструкции, а не в адресе, на который она ссылается. Например:
1 | bash[1234] trap invalid opcode ip:48db90 sp:7ffddc8879e8 error:0 in bash[400000+f4000] |
В файле traps.c есть целый ряд подобных типов ловушек. Два заметных дополнительных - это 'ошибка деления', которую вы получите, если выполните целочисленное деление на ноль, и 'общая защита', которую вы можете получить для некоторых чрезвычайно диких указателей (один известный мне случай - когда ваш 64-битный x86-адрес не имеет "канонической формы"). Хотя эти поля имеют несколько иной формат, большинство из них означают то же самое, что и в segfaults. Исключением является 'error:0', который не является кодом ошибки страничного сбоя.
Иногда эти сообщения могут быть немного необычными и удивительными. Вот пример глупой программы и ошибки, которую она выдает при выполнении. Код:
1 2 3 4 5 6 | #include <stdio.h> int main(int argc, char **argv) { int (*p)(); p = 0x0; return printf("%d\n", (*p)()); } |
Если скомпилировать (лучше всего без оптимизации) и запустить, это сгенерирует сообщение ядра:
1 | a.out[3715]: segfault at 0 ip (null) sp 00007fde872aa418 error 14 in a.out[400000+1000] |
Бит '(null)' оказался ожидаемым; это то, что генерирует функция общего ядра printf(), когда ее просят вывести что-то в виде указателя, а он равен нулю (как показано здесь). В нашем случае указатель инструкции равен 0 (null), потому что мы сделали вызов подпрограммы через нулевой указатель и таким образом пытаемся выполнить код по адресу 0. Я не знаю, почему часть 'in ...' говорит, что мы находимся в исполняемом файле (хотя в данном случае вызов действительно был там).
Код ошибки 14 в шестнадцатеричном формате, что означает, что в битах это 010100. Это чтение немаркированной области в режиме пользователя (наш обычный случай '4'), но это выборка инструкций, а не обычное чтение или запись данных. Любая ошибка 14 является признаком какого-либо искаженного вызова функции или возврата по искаженному адресу, поскольку стек был поврежден.
Для 64-битных x86 ядер Linux (и, возможно, для 32-битных x86 тоже), код, на который вы хотите посмотреть, это show_signal_msg в fault.c, который печатает общее сообщение 'segfault at ...', do_trap и do_general_protection в traps.c, которые печатают сообщения 'trap ...', и print_vma_addr в memory.c, который печатает часть 'in ...' для всех этих сообщений.
Различные биты кода ошибки в виде чисел
- +1 ошибка защиты в сопоставленной области (например, запись в сопоставление, доступное только для чтения)
- +2 запись (вместо чтения)
- +4 доступ в режиме пользователя (вместо доступа в режиме ядра)
- +8 обнаружено использование зарезервированных битов в записи таблицы страниц (ядро будет паниковать, если это произойдет)
- +16 (+0x10) ошибка была выборкой инструкции, а не чтением или записью данных
- +32 (+0x20) 'доступ к блоку ключей защиты'.
Hex 0x14 - это 0x10 + 4; (hex) 6 - это 4 + 2. Код ошибки 7 (0x7) - это 4 + 2 + 1, запись в режиме пользователя в отображение, доступное только для чтения, и это то, что вы получите, если попытаетесь записать в строковую константу в C:
1 2 3 4 | char *ex = "example"; int main(int argc, char **argv) { *ex = 'E'; } |
Скомпилируйте и запустите это, и вы получите:
1 | a.out[8832]: segfault at 400540 ip 0000000000400489 sp 00007ffce6831470 error 7 in a.out[400000+1000]. |
Похоже, что программный код всегда загружается по адресу 0x400000 для обычных программ, хотя у разделяемых библиотек их расположение может быть рандомизировано.
Согласно комментарию в исходном тексте ядра, все обращения к адресам выше конца пользовательского пространства будут помечены как "сбой защиты в отображенной области", независимо от того, есть ли там реальные записи в таблице страниц. Ядро делает это для того, чтобы вы не могли определить, где находятся его страницы памяти, глядя на код ошибки.