C main
вызывается (косвенно) из кода запуска CRT, а не напрямую из ядра.
После возврата main
этот код вызывает atexit
функции для выполнения таких вещей, как сброс stdio буферизует, затем передает возвращаемое значение main в необработанный системный вызов _exit
. Или exit_group
, который выходит из всех потоков.
Вы делаете несколько неправильных предположений, все, что я думаю, основываясь на неправильном понимании того, как работают ядра.
Ядро работает на уровне привилегий, отличном от пространства пользователя (кольцо 0 против кольца 3 на x86). Даже если пользовательское пространство знает правильный адрес для перехода, оно не может перейти в код ядра. (И даже если бы это было возможно, он не работал бы с ядром уровень привилегий ).
ret
не маги c, это просто pop %rip
и не не позволяйте прыгать туда, куда вы не могли перейти с другими инструкциями. Также не изменяет уровень привилегий 1 .
Адреса ядра не отображаются / не доступны, когда выполняется код пользовательского пространства; эти записи таблицы страниц помечены как только для супервизора. (Или они вообще не отображаются в ядрах, которые уменьшают уязвимость Meltdown, поэтому вход в ядро проходит через блок кода-оболочки, который изменяет CR3.)
Виртуальная память - это то, как ядро защищает себя от пространства пользователя. пространство пользователя не может изменять таблицы страниц напрямую, только попросив ядро сделать это с помощью системных вызовов mmap
и mprotect
. (И пользовательское пространство не может выполнять привилегированные инструкции, такие как mov cr3, rax
, для установки новых таблиц страниц. Именно поэтому кольцо 0 (режим ядра) и кольцо 3 (режим пользователя).)
Стек ядра отделен от стека пользовательского пространства для процесса . (В ядре также имеется небольшой стек ядра для каждой задачи (иначе говоря, потока), который используется во время системных вызовов / прерываний во время работы этого потока в пользовательском пространстве. По крайней мере, именно так Linux делает это, IDK о других.)
Ядро буквально не call
код пользовательского пространства; Стек пользовательского пространства не удерживает обратный адрес в ядре. Переход ядра-> пользователя включает в себя обмен указателями стека, а также изменение уровней привилегий. например, с такой инструкцией, как iret
(прерывание-возврат).
Плюс, оставляя адрес кода ядра везде, где пользовательское пространство может видеть, что это может победить ASLR ядра.
Сноска 1: (Сгенерированный компилятором ret
всегда будет нормальным рядом с ret
, а не retf
, который может вернуться через шлюз вызова или что-то к привилегированному значению cs
. x86 обрабатывает уровни привилегий через младшие 2 бита CS, но не обращайте на это внимания. MacOS / Linux не не устанавливает шлюзы вызовов, которые пользовательское пространство может использовать для вызова в ядро; это сделано с syscall
или int 0x80
инструкции.)
В процессе fre sh (после того, как системный вызов execve
заменил предыдущий процесс на этот PID новым), выполнение начинается с процесса точка входа (обычно обозначается _start
), , а не в функции C main
напрямую.
C реализации поставляются с кодом запуска CRT (C RunTime), который имеет (среди прочего) рукописную реализацию asm * 1 074 * который (косвенно) вызывает main
, передавая аргументы в main согласно соглашению о вызовах.
_start
само по себе не является функцией. При входе в процесс RSP указывает на argc
, и выше, что в стеке пользовательского пространства равно argv[0]
, argv[1]
, и др c. (т. е. массив char *argv[]
находится прямо по значению и выше массива envp
.) _start
загружает argc
в регистр и помещает указатели на argv и envp в регистры. ( x86-64 System V ABI, в котором MacOS и Linux оба используют документы, все это, включая среду запуска процесса и соглашение о вызовах. )
Если вы попытаетесь до ret
из _start
, вы просто вставите argc
в RIP, а затем получите код с абсолютного адреса 1
или 2
( или другое небольшое количество) будет segfault. Например, Ошибка сегментации носа на RET в _start показывает попытку ret
из точки входа в процесс (связанный без кода запуска CRT). У него есть рукописный текст _start
, который просто записывается в main
.
Когда вы запускаете gcc main.c
, интерфейс gcc
запускает несколько других программ (используйте gcc -v
показать детали). Вот как код запуска CRT связывается с вашим процессом:
- g cc препроцессирует (CPP) и компилирует + собирает
main.c
в main.o
(или временный файл). В MacOS команда gcc
на самом деле является clang, у которой есть встроенный ассемблер, но настоящий gcc
действительно компилируется в asm и затем запускает as
на этом. (Тем не менее, препроцессор C встроен в компилятор.) - g cc выполняет что-то вроде
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie /usr/lib/Scrt1.o /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/crtbeginS.o main.o -lc -lgcc /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/crtendS.o
. Это на самом деле упрощено много , некоторые файлы CRT пропущены, а пути канонизированы для удаления ../../lib
деталей. Кроме того, он не запускает ld
напрямую, он запускает collect2
, который является оболочкой для ld
. Но в любом случае, это статически связывает в тех .o
CRT-файлах, которые содержат _start
и некоторые другие вещи, и динамически связывает lib c (-lc
) и libg cc (для вспомогательных функций G CC, таких как реализация __int128
умножать и делить на 64-битные регистры, если ваша программа их использует).
.intel_syntax
.text:
.global _rbp
_rbp:
mov rax, rbp
ret;
Это недопустимо, ...
Единственная причина, по которой не выполняется сборка, заключается в том, что вы пытались объявить .text:
как метку вместо использования директивы .text
. Если вы удалите конечный :
, он собирается с помощью clang (который обрабатывает .intel_syntax
так же, как .intel_syntax noprefix
).
Для G CC / GAS для его сборки вам также понадобится noprefix
, чтобы сообщить ему, что именам регистров не предшествует %
. (Да, вы можете иметь Intel op dst, sr c порядок, но все еще с %rsp
именами регистров. Нет, вы не должны делать это!) И, конечно, GNU / Linux не использует начальные подчеркивания.
Не то чтобы оно всегда делало то, что вы хотите, если бы вы его назвали! Если вы скомпилировали main
без оптимизации (так что -fno-omit-frame-pointer
действовал), тогда да, вы получите указатель на слот стека под адресом возврата.
И вы определенно неправильно использовать значение . (*p)-4;
загружает сохраненное значение RBP (*p
) и затем смещается четырьмя 8-байтовыми пустыми указателями. (Потому что так работает C математика указателя; *p
имеет тип void*
, потому что p
имеет тип void **
).
Я думаю, вы пытаетесь получить свой собственный обратный адрес и пере - запустить команду call
(в вызывающей программе main), которая достигла main, что в конечном итоге привело к переполнению стека из-за нажатия большего количества адресов возврата. В GNU C используйте void * __builtin_return_address (0)
, чтобы получить свой собственный обратный адрес .
x86 call rel32
инструкции имеют 5 байтов, но call
, который вызывал main, был возможно косвенный вызов с использованием указателя в регистре . Так что это может быть 2-байтовый call *%rax
или 3-байтовый call *%r12
, вы не узнаете, пока не разберете своего вызывающего. (Я бы предложил пошаговое выполнение инструкций (GDB / LLDB stepi
) до конца main
с использованием отладчика в режиме разборки. Если у него есть какая-либо символьная информация для вызывающего абонента main, вы сможете прокрутить назад и посмотрите, что было в предыдущей инструкции.
Если нет, то вам, возможно, придется попробовать и посмотреть, что выглядит вменяемым; машинный код x86 не может быть однозначно декодирован в обратном направлении, потому что он имеет переменную длину. Разница между байтом в инструкции (например, немедленным или ModRM) и началом инструкции. Все зависит от того, откуда вы начинаете разборку. Если вы попытаетесь сместить несколько байтов, обычно только один производить все, что выглядит вменяемым.
asm("movq %rax, 0"); //Exit code is 11, so now it should be 0
Это хранилище RAX по абсолютному адресу 0
в синтаксисе AT & T. Это, конечно, segfaults. код выхода 11 от SIGSEGV, который является сигналом 11. (Используйте kill -l
, чтобы увидеть номера сигналов).
Возможно, вы хотели mov $0, %eax
. Хотя это все еще бессмысленно, вы собираетесь вызывать через указатель на функцию. В режиме отладки компилятор может загрузить его в RAX и изменить ваше значение.
Кроме того, запись регистра в операторе asm
никогда не бывает безопасной, если вы не сообщаете компилятору о том, какие регистры вы используете. изменение (использование ограничений).
printf("Main: %p\n", main);
printf("&Main: %p\n", &main); //WTF
main
и &main
- это одно и то же, потому что main
является функцией. Вот как работает синтаксис C для имен функций. main
не является объектом, который может получить свой адрес. & оператор необязательный при назначении указателя на функцию
Это аналогично для массивов: голое имя массива может быть назначено указателю или передано в функции в качестве аргумента-указателя. Но &array
тоже тот же указатель, что и &array[0]
. Это верно только для массивов , таких как int array[10]
, но не для указателей, таких как int *ptr
; в последнем случае сам объект-указатель имеет место для хранения и может иметь собственный адрес.