C main
从 CRT 启动代码(间接)调用,而不是直接从内核调用。
After main
返回,该代码调用atexit
函数执行诸如刷新 stdio 缓冲区之类的操作,然后将 main 的返回值传递给原始值_exit
系统调用。或者exit_group
它退出所有线程。
您做出了几个错误的假设,我认为这些假设都是基于对内核工作原理的误解。
-
内核以与用户空间不同的权限级别运行(x86 上的环 0 与环 3)。即使用户空间知道要跳转到的正确地址,它也无法跳转到内核代码。 (即使可以,它也不会与内核一起运行特权级别).
ret
isn't magic, it's basically just pop %rip
and doesn't let you jump anywhere you couldn't jump to with other instructions. Also doesn't change privilege level1.
-
当用户空间代码运行时,内核地址无法映射/访问;这些页表条目被标记为仅限主管。 (或者它们根本没有映射到缓解 Meltdown 漏洞的内核中,因此进入内核会经过一个更改 CR3 的“包装”代码块。)
虚拟内存是内核保护自身免受用户空间影响的方式。用户空间不能直接修改页表,只能通过请求内核来完成mmap
and mprotect
系统调用。 (并且用户空间无法执行特权指令,例如mov cr3, rax
安装新的页表。这就是设置环 0(内核模式)与环 3(用户模式)的目的。)
对于进程来说,内核堆栈与用户空间堆栈是分开的。 (在内核中,每个任务(也称为线程)还有一个小的内核堆栈,在用户空间线程运行时在系统调用/中断期间使用。至少 Linux 是这样做的,不知道其他的。)
-
内核并不是字面上的意思call
用户空间代码;用户空间堆栈不会将任何返回地址保留回内核。内核->用户转换涉及交换堆栈指针以及更改特权级别。例如用类似的指令iret https://www.felixcloutier.com/x86/iret:iretd(中断返回)。
另外,将内核代码地址留在用户空间可以看到的任何地方都会破坏内核 ASLR。
脚注 1:(编译器生成的ret
永远是正常的附近ret
, not a retf
可以通过调用门或其他方式返回给特权者cs
价值。 x86 通过 CS 的低 2 位处理权限级别,但没关系。 MacOS / Linuxdon't设置用户空间可以用来调用内核的调用门;完成了syscall
or int 0x80
指示。)
在一个新鲜的过程中(经过execve
系统调用用新的 PID 替换了前一个进程),执行从进程入口点开始(通常标记为_start
), not在Cmain
直接运行。
C 实现附带 CRT(C 运行时)启动代码,该代码(除其他外)有一个手写的 asm 实现_start
(间接)调用main
,根据调用约定将 args 传递给 main。
_start
本身不是一个函数。在流程输入时,RSP 指向argc
,上面的用户空间堆栈上是argv[0]
, argv[1]
等(即char *argv[]
数组按值就在那里,上面是envp
大批。)_start
loads argc
放入寄存器并将指向 argv 和 envp 的指针放入寄存器中。 (MacOS 和 Linux 都使用的 x86-64 System V ABI 记录了所有这些,包括进程启动环境和调用约定。)
If you try to ret
from _start
,你就会弹出argc
进入RIP,然后从绝对地址取码1
or 2
(或其他少量)将出现段错误。例如,_start 中 RET 上的 Nasm 分段错误 https://stackoverflow.com/questions/19760002/nasm-segmentation-fault-on-ret-in-start表明尝试ret
从进程入口点(链接withoutCRT 启动代码)。它有一个手写的_start
刚刚落入main
.
当你跑步时gcc main.c
, the gcc
前端运行多个其他程序(使用gcc -v
以显示详细信息)。这就是 CRT 启动代码链接到您的进程的方式:
- gcc 预处理器 (CPP) 和编译+程序集
main.c
to main.o
(或临时文件)。在 MacOS 上,gcc
命令实际上是 clang,它有一个内置的汇编器,但是真实的gcc
确实编译成asm然后运行as
关于这一点。 (不过,C 预处理器内置于编译器中。)
- gcc 运行类似的东西
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
。这实际上是简化的a lot,省略了一些 CRT 文件,并对路径进行了规范化以删除../../lib
部分。另外,它不运行ld
直接运行collect2
这是一个包装器ld
。但无论如何,静态链接在那些.o
CRT 文件包含_start
和其他一些东西,并动态链接 libc (-lc
)和 libgcc (用于 GCC 辅助函数,例如实现__int128
使用 64 位寄存器进行乘法和除法(如果您的程序使用这些寄存器)。
.intel_syntax
.text:
.global _rbp
_rbp:
mov rax, rbp
ret;
这是不允许的,...
不组装的唯一原因是因为你试图声明.text:
作为标签,而不是使用.text
指示。如果删除尾随:
它确实用 clang 进行组装(它对待.intel_syntax
与.intel_syntax noprefix
).
对于 GCC / GAS 来组装它,您还需要noprefix
告诉它寄存器名称没有前缀%
。 (是的,就是你can有 Intel op dst、src 顺序,但仍然有%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)
获取您自己的退货地址 https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html.
x86 call rel32
指令是5个字节,但是call
调用 main 可能是间接调用,使用寄存器中的指针。所以它可能是一个2字节call *%rax
或 3 字节call *%r12
,除非你反汇编你的调用者,否则你不知道。 (我建议按指令单步执行(GDB / LLDBstepi
)结束main
在反汇编模式下使用调试器。如果它有 main 调用者的任何符号信息,您将能够向后滚动并查看上一条指令是什么。
如果没有,你可能必须尝试看看什么看起来是正常的; x86 机器代码无法明确地向后解码,因为它是可变长度的。您无法区分指令中的字节(例如立即数或 ModRM)与指令的开头之间的区别。这完全取决于你在哪里start拆解自.如果您尝试几个字节偏移,通常只有一个会产生看起来正常的结果。
asm("movq %rax, 0"); //Exit code is 11, so now it should be 0
这是 RAX 到绝对地址的存储0
, 在 AT&T 语法中。这当然会出现段错误。退出代码 11 来自 SIGSEGV,即信号 11。(使用kill -l
查看信号编号)。
也许你想要mov $0, %eax
。尽管这在这里仍然毫无意义,但您将通过函数指针进行调用。在调试模式下,编译器可能会将其加载到 RAX 中并逐步执行您的值。
另外,在一个寄存器中写入一个asm
当您不告诉编译器您正在修改哪些寄存器(使用约束)时,语句永远不会安全。
printf("Main: %p\n", main);
printf("&Main: %p\n", &main); //WTF
main
and &main
是同一件事,因为main
是一个函数。这就是 C 语法对函数名称的作用。main
不是一个可以获取其地址的对象。
数组的情况类似:数组的裸名称可以分配给指针或作为指针 arg 传递给函数。但&array
也是同一个指针,同&array[0]
。这仅适用于arrays like int array[10]
,不适用于像这样的指针int *ptr
;在后一种情况下,指针对象本身具有存储空间并且可以获取其自己的地址。