我即将弄清楚程序堆栈是如何设置的。 我了解到调用函数
call pointer;
实际上与以下相同:
mov register, pc ;programcounter
add register, 1 ; where 1 is one instruction not 1 byte ...
push register
jump pointer
但是,这意味着当 Unix 内核调用 main 函数时,堆栈基础应指向调用 main 的内核函数中的重新进入。
因此,在 C 代码中跳转 "*rbp-1" 应该重新进入主函数。
但是,这不是以下代码中发生的情况:
#include <stdlib.h>
#include <unistd.h>
extern void ** rbp(); //pointer to stack pointing to function
int main() {
void ** p = rbp();
printf("Main: %pn", main);
printf("&Main: %pn", &main); //WTF
printf("*Main: %pn", *main); //WTF
printf("Stackbasepointer: %pn", p);
int (*c)(void) = (*p)-4;
asm("movq %rax, 0");
c();
return 0; //should never be executed...
}
程序集文件: rsp.asm
。
.intel_syntax
.text:
.global _rbp
_rbp:
mov rax, rbp
ret;
这是不允许的,不出所料,也许是因为此时的指令不是正好是 64 位,也许是因为 UNIX 不允许这样做......
但也不允许此调用:
void (*c)(void) = (*p);
asm("movq %rax, 0"); //Exit code is 11, so now it should be 0
c(); //this comes with stack corruption, when successful
这意味着我没有义务退出主调用函数。
那么我的问题是:为什么当我使用每个 GCC 主函数末尾看到的 ret 时,它应该有效地与上面的代码相同。Unix - 系统如何有效地检查此类尝试... 我希望我的问题很清楚...
谢谢。 PS:代码只能在macOS上编译,更改Linux的汇编
Cmain
是从 CRT 启动代码(间接)调用的,而不是直接从内核调用的。
main
返回后,该代码调用atexit
函数来执行刷新 stdio 缓冲区等操作,然后将 main 的返回值传递给原始_exit
系统调用。 或者exit_group
退出所有线程。
你做了几个错误的假设,我认为所有这些都是基于对内核工作原理的误解。
-
内核以与用户空间不同的权限级别运行(环 0 与环 3 在 x86 上)。 即使用户空间知道要跳转到的正确地址,它也无法跳转到内核代码。 (即使可以,它也不会以内核权限级别运行)。
ret
不是魔法,它基本上只是pop %rip
,不会让你跳到任何你不能跳到其他指令的地方。 也不会更改权限级别1。 -
运行用户空间代码时,内核地址不被映射/访问;这些页表条目被标记为仅限主管。 (或者它们根本没有映射到缓解 Meltdown 漏洞的内核中,因此进入内核会通过更改 CR3 的"包装器"代码块。
虚拟内存是内核保护自己免受用户空间影响的方式。用户空间不能直接修改页表,只能通过
mmap
和mprotect
系统调用要求内核来修改。 (并且用户空间无法执行特权指令(如mov cr3, rax
来安装新的页表)。 这就是环 0(内核模式)与环 3(用户模式)的目的。 -
内核堆栈独立于进程的用户空间堆栈。 (在内核中,每个任务(又名线程)还有一个小的内核堆栈,在该用户空间线程运行时的系统调用/中断期间使用。 至少Linux是这样做的,IDK是关于其他人的。
-
内核并没有从字面上
call
用户空间代码;用户空间堆栈不会将任何返回地址保留回内核。内核>用户转换涉及交换堆栈指针以及更改权限级别。 例如,使用像iret
(中断-返回)这样的指令。另外,在用户空间可以看到的地方留下内核代码地址会破坏内核 ASLR。
脚注 1:(编译器生成的ret
将始终是正常的近ret
,而不是可以通过调用门或其他东西返回到特权cs
值的retf
。 x86 通过低 2 位 CS 处理权限级别,但没关系。 MacOS/Linux没有设置用户空间可以用来调用内核的呼叫门;这是通过syscall
或int 0x80
说明完成的。
一个新的进程中(在execve
系统调用用新 PID 替换以前的进程之后),执行从进程入口点(通常标记为_start
)开始,而不是直接在 Cmain
函数开始。
C 实现附带 CRT(C 运行时)启动代码,该代码具有(除其他外)手写的 asm 实现_start
,它(间接)调用main
,根据调用约定将参数传递给 main。
_start
本身不是一个函数。在进程进入时,RSP 指向argc
,高于用户空间堆栈的argv[0]
、argv[1]
等。 (即char *argv[]
数组按值就在那里,envp
数组高于该值。_start
argc
加载到寄存器中,并将指向 argv 和 envp 的指针放入寄存器中。 (MacOS 和 Linux 都使用的 x86-64 System V ABI 记录了所有这些,包括进程启动环境和调用约定。
如果您尝试从_start
ret
,您只需将argc
弹出到 RIP 中,然后从绝对地址1
或2
(或其他少量数字)获取代码将出现段错误。 例如,_start 中 RET 上的 Nasm 分段错误显示尝试从进程入口点(在没有 CRT 启动代码的情况下链接)ret
。 它有一个手写的_start
,刚刚落入main
。
当您运行gcc main.c
时,gcc
前端运行多个其他程序(使用gcc -v
显示详细信息)。 以下是 CRT 启动代码链接到流程的方式:
- gcc 预处理 (CPP) 并编译 + 汇编
main.c
到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
. 这实际上简化了很多,省略了一些 CRT 文件,并且路径规范化以删除../../lib
部分。 此外,它不直接运行ld
,它运行collect2
这是ld
的包装器。 但无论如何,它会静态链接那些包含_start
和其他一些东西的.o
CRT 文件,并动态链接 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
告诉它寄存器名称不以%
为前缀。 (是的,您可以使用英特尔操作 dst、src 顺序,但仍具有%rsp
寄存器名称。 不,你不应该这样做! 当然,GNU/Linux 不使用前导下划线。
不过,并不是说如果你叫它,它总是会做你想做的事! 如果您在没有优化的情况下编译main
(因此-fno-omit-frame-pointer
有效),那么是的,您将获得指向返回地址下方堆栈插槽的指针。
而且您肯定错误地使用了该值。(*p)-4;
加载保存的RBP值(*p
),然后偏移四个8字节的空指针。 (因为这就是 C 指针数学的工作方式;*p
具有类型void*
,因为p
具有类型void **
)。
我认为您正在尝试获取自己的返回地址并重新运行到达 main 的call
指令(在 main 的调用者中),最终导致推送更多返回地址导致堆栈溢出。 在 GNU C 中,使用void * __builtin_return_address (0)
获取您自己的退货地址。
x86call rel32
指令为 5 个字节,但调用 main 的call
可能是间接调用,使用寄存器中的指针。 所以它可能是 2 字节call *%rax
或 3 字节call *%r12
,除非您拆卸调用方,否则您不知道。 (我建议在反汇编模式下使用调试器在main
结束时按指令(GDB/LLDBstepi
)单步执行。 如果它有任何主调用方的符号信息,您将能够向后滚动并查看上一条指令是什么。
如果没有,你可能不得不尝试看看什么看起来很合理;x86机器码不能明确地向后解码,因为它是可变长度的。 您无法区分指令中的字节(如即时或 ModRM)与指令的开头。 这完全取决于您从哪里开始拆卸。 如果您尝试几个字节偏移量,通常只有一个会产生任何看起来正常的东西。
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: %pn", main);
printf("&Main: %pn", &main); //WTF
main
和&main
是一回事,因为main
是一个函数。 这就是 C 语法对函数名称的工作方式。main
不是可以获取其地址的对象。 和运算符在函数指针赋值中可选
数组也是如此:数组的裸名称可以分配给指针或作为指针参数传递给函数。 但&array
也是相同的指针,与&array[0]
相同。 这仅适用于像int array[10]
这样的数组,不适用于像int *ptr
这样的指针;在后一种情况下,指针对象本身具有存储空间,并且可以获取自己的地址。
我认为你在这里有很多误解。首先,main
不是内核所称的。内核分配一个进程并将我们的二进制文件加载到内存中 - 如果您使用的是基于 Unix 的操作系统,通常来自 ELF 文件。这个ELF文件包含所有需要映射到内存中的部分和一个地址,该地址是ELF中代码的"入口点"(以及其他内容)。ELF 可以指定加载器跳转到的任何地址,以便开始启动程序。在使用 GCC 构建的应用程序中,这是一个称为_start
的函数。 然后_start
设置堆栈并在调用之前执行任何其他初始化__libc_start_main
这是一个 libc 函数,可以在调用主main
之前进行额外的设置。
下面是一个启动函数的示例:
00000000000006c0 <_start>:
6c0: 31 ed xor %ebp,%ebp
6c2: 49 89 d1 mov %rdx,%r9
6c5: 5e pop %rsi
6c6: 48 89 e2 mov %rsp,%rdx
6c9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
6cd: 50 push %rax
6ce: 54 push %rsp
6cf: 4c 8d 05 0a 02 00 00 lea 0x20a(%rip),%r8 # 8e0 <__libc_csu_fini>
6d6: 48 8d 0d 93 01 00 00 lea 0x193(%rip),%rcx # 870 <__libc_csu_init>
6dd: 48 8d 3d 7c ff ff ff lea -0x84(%rip),%rdi # 660 <main>
6e4: ff 15 f6 08 20 00 callq *0x2008f6(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
6ea: f4 hlt
6eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
如您所见,此函数设置堆栈和堆栈基指针的值。因此,此函数中没有有效的堆栈帧。堆栈帧甚至不会设置为 0 以外的任何值,直到您调用main
(至少通过此编译器)
现在这里要看到的重要一点是,堆栈是在此代码中初始化的,并且由加载器初始化,它不是内核堆栈的延续。每个程序都有自己的堆栈,这些堆栈都与内核的堆栈不同。事实上,即使您知道内核中堆栈的地址,也无法从程序中读取或写入堆栈,因为您的进程只能看到由内核控制的 MMU 分配给它的内存页。
澄清一下,当我说堆栈是"创建"的时,我并不是说它被分配了。我的意思只是堆栈指针和堆栈基础设置在这里。它的内存在加载程序时分配,每当写入堆栈的未分配部分触发页面错误时,都会根据需要将页面添加到程序中。进入 start 后,显然存在一些堆栈作为来自pop rsi
指令的证据,但这不是程序将使用的最终堆栈值的堆栈。这些是在_start
中设置的变量(也许这些变量稍后会在__libc_start_main
中更改,我不确定。
但是,这意味着当 Unix 内核调用 main 函数时,堆栈基础应该指向调用 main 的内核函数中的重新进入。
绝对不行。
这个特定问题涵盖了MacOS的详细信息,请查看。在任何情况下,main 最有可能返回 C 标准库的启动功能。 实现细节因不同的 *nix 操作系统而异。
因此,在 C 代码中跳转 "*rbp-1" 应该重新进入主函数。
您无法保证编译器将发出什么以及调用rbp()
函数时 rsp/rbp 的状态。你不能做出这样的假设。
顺便说一句,如果您想以 64 位访问堆栈条目,您将以 +-8 的增量执行此操作(分别rbp+8
rbp-8
rsp+8
rsp-8
)。