为什么允许我使用ret退出main? [英] Why am I allowed to exit main using ret?

查看:58
本文介绍了为什么允许我使用ret退出main?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我将弄清楚如何正确设置程序堆栈.我了解到,使用

I am about to figure out how exactly a programm stack is set up. I have learned that calling the function with

call pointer;

实际上与以下内容相同:

Is effectively the same as:

mov register, pc ;programcounter
add register, 1 ; where 1 is one instruction not 1 byte ...
push register
jump pointer

但是,这意味着当Unix内核调用main函数时,堆栈库应该指向重新进入调用main的内核函数.

However, this would mean that when the Unix Kernel calls the main function that the stack base should point to reentry in the kernel function which calls main.

因此,在C代码中跳转"* rbp-1"应重新输入主要功能.

Therefore jumping "*rbp-1" in the C - Code should reenter the main function.

但是,以下代码中不会发生这种情况:

This, however, is not what happens in the following code:

#include <stdlib.h>
#include <unistd.h>

extern void ** rbp(); //pointer to stack pointing to function
int main() {
   void ** p = rbp();
   printf("Main: %p\n", main);
   printf("&Main: %p\n", &main); //WTF
   printf("*Main: %p\n", *main); //WTF
   printf("Stackbasepointer: %p\n", p);
   int (*c)(void) = (*p)-4;
   asm("movq %rax, 0");
   c();

   return 0;        //should never be executed...

}

汇编文件:rsp.asm

Assembly file: rsp.asm

...

.intel_syntax

.text:

.global _rbp

_rbp:
  mov rax, rbp
  ret;

毫不奇怪,这是不允许的,也许是因为此时的指令不完全是64位,也许是因为UNIX不允许这样做...

This is not allowed, unsurprisingly, maybe because the instruction at this point are not exactly 64 bits, maybe because UNIX does not allow this...

不允许该呼叫:

   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

这意味着我没有义务退出主调用函数.

This means I am not obliged to exit the main - calling function.

然后我的问题是:为什么我在每个GCC主函数的末尾都使用ret时要使用ret?它的作用应该与上面的代码相同Unix-系统如何有效地检查此类尝试...我希望我的问题很清楚...

My question then is: Why am I when I use ret as seen in the end of every GCC main function?, which should do effectively the same as the code above. How does a unix - system check for such attempts effectively... I hope my question is clear...

谢谢.P.S .:代码仅在macOS上编译,为Linux更改程序集

Thank you. P.S.: Code compiles only on macOS, change assembly for linux

推荐答案

C main 是从CRT启动代码(而不是直接从内核)中间接调用的.

C main is called (indirectly) from CRT startup code, not directly from the kernel.

main 返回后,该代码将调用 atexit 函数执行诸如刷新stdio缓冲区之类的操作,然后将main的返回值传递给原始的 _exit 系统调用.或退出所有线程的 exit_group .

After main returns, that code calls atexit functions to do stuff like flushing stdio buffers, then passes main's return value to a raw _exit system call. Or exit_group which exits all threads.

你做出了几个错误的假设,我认为都是基于对内核工作原理的误解.

You make several wrong assumptions, all I think based on a misunderstanding of how kernels work.

  • 内核以与用户空间不同的特权级别运行(x86上的环0与环3).即使用户空间知道要跳转的正确地址,也无法跳转到内核代码中.(即使可以,它也不会与内核特权级别一起运行).

ret 不是魔术,它基本上只是 pop%rip ,并且不允许您跳转到其他说明无法跳转的任何地方.也不会更改特权级别 1 .

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.)

Kernel addresses aren't mapped / accessible when user-space code is running; those page-table entries are marked as supervisor-only. (Or they're not mapped at all in kernels that mitigate the Meltdown vulnerability, so entering the kernel goes through a "wrapper" block of code that changes CR3.)

虚拟内存是内核如何保护自己免受用户空间的侵害.用户空间无法直接修改页表,只能通过要求内核通过 mmap 和 mprotect 系统调用.(而且用户空间不能执行 mov cr3,rax 之类的特权指令来安装新的页表.这是为了使环0(内核模式)与环3(用户模式)相对应.)

Virtual memory is how the kernel protects itself from user-space. User-space can't modify page tables directly, only by asking the kernel to do it via mmap and mprotect system calls. (And user-space can't execute privileged instructions like mov cr3, rax to install new page tables. That's the purpose of having ring 0 (kernel mode) vs. ring 3 (user mode).)

内核堆栈与进程的用户空间堆栈是分开的.(在内核中,每个用户空间线程运行时,在系统调用/中断期间使用的每个任务(aka线程)都有一个小的内核堆栈.至少Linux是这样做的,IDK是关于其他任务的.)

The kernel stack is separate from the user-space stack for a process. (In the kernel, there's also a small kernel stack for each task (aka thread) that's used during system calls / interrupts while that user-space thread is running. At least that's how Linux does it, IDK about others.)

内核实际上并不调用用户空间代码;用户空间堆栈不会将任何返回地址保存回内核.内核到用户的转换涉及交换堆栈指针以及更改特权级别.例如使用 iret (中断返回)之类的指令.

The kernel doesn't literally call user-space code; The user-space stack doesn't hold any return address back into the kernel. A kernel->user transition involves swapping stack pointers, as well as changing privilege levels. e.g. with an instruction like iret (interrupt-return).

此外,将内核代码地址保留在用户空间可以看到的任何地方,它将击败内核ASLR.

Plus, leaving a kernel code address anywhere user-space can see it would defeat kernel ASLR.

脚注1 :(编译器生成的 ret 将始终是 ret 附近的正常位置,而不是可能通过调用返回的 retf x86通过CS的低2位来处理特权级别,但请不要忘记MacOS/Linux 不要设置该用户的呼叫门-space可用于调用内核;这是通过 syscall int 0x80 指令完成的.)

Footnote 1: (The compiler-generated ret will always be a normal near ret, not a retf that could return through a call gate or something to a privileged cs value. x86 handles privilege levels via the low 2 bits of CS but nevermind that. MacOS / Linux don't set up call gates that user-space can use to call into the kernel; that's done with syscall or int 0x80 instructions.)

在一个新进程中(在 execve 系统调用之后,该进程用这个PID用新的PID替换了该进程),执行从进程入口点开始(通常标记为 _start >),直接在C main 函数中 not .

In a fresh process (after an execve system call replaced the previous process with this PID with a new one), execution begins at the process entry point (usually labeled _start), not at the C main function directly.

C实现随附CRT(C运行时)启动代码,该代码具有(除其他外)手写的 _start 的asm实现,(间接)称为 main ,根据调用约定将args传递给main.

C implementations come with CRT (C RunTime) startup code that has (among other things) a hand-written asm implementation of _start which (indirectly) calls main, passing args to main according to the calling convention.

_start 本身不是函数.在进程进入时,RSP指向 argc ,而在用户空间堆栈上则是 argv [0] argv [1] 等(例如,按值在 char * argv [] 数组的正上方) _envp 数组.) _start argc 加载到寄存器中,并将指向argv和envp的指针放入寄存器中.( MacOS和Linux都使用的x86-64 System V ABI记录了所有这些文档,包括进程启动环境和调用约定.)

_start itself is not a function. On process entry, RSP points at argc, and above that on the user-space stack is argv[0], argv[1], etc. (i.e. the char *argv[] array is right there by value, and above that the envp array.) _start loads argc into a register and puts pointers to the argv and envp into registers. (The x86-64 System V ABI that MacOS and Linux both use documents all this, including the process-startup environment and the calling convention.)

如果您尝试 _start ret ,则只需将 argc 弹出到RIP中,然后从绝对地址 1 2 (或其他小数字)中提取代码将导致段错误.例如,在_start中RET上的nasm分段错误表示试图从流程入口点 ret (链接了而没有 CRT启动代码).它有一个手写的 _start ,可以直接进入 main .

If you try to ret from _start, you're just going to pop argc into RIP, and then code-fetch from absolute address 1 or 2 (or other small number) will segfault. For example, Nasm segmentation fault on RET in _start shows an attempt to ret from the process entry point (linked without CRT startup code). It has a hand-written _start that just falls through into main.

运行 gcc main.c 时, gcc 前端会运行其他多个程序(使用 gcc -v 显示详细信息).这是将CRT启动代码链接到您的过程的方式:

When you run gcc main.c, the gcc front-end runs multiple other programs (use gcc -v to show details). This is how the CRT startup code gets linked into your process:

  • 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位寄存器相乘和除法,以防您的程序使用它们).
  • gcc preprocesses (CPP) and compiles+assembles main.c to main.o (or a temporary file). On MacOS, the gcc command is actually clang which has a built-in assembler, but real gcc really does compile to asm and then run as on that. (The C preprocessor is built-in to the compiler, though.)
  • gcc runs something like 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. That's actually simplified a lot, with some of the CRT files left out, and paths canonicalized to remove ../../lib parts. Also, it doesn't run ld directly, it runs collect2 which is a wrapper for ld. But anyway, that statically links in those .o CRT files that contain _start and some other stuff, and dynamically links libc (-lc) and libgcc (for GCC helper functions like implementing __int128 multiply and divide with 64-bit registers, in case your program uses those).
.intel_syntax

.text:

.global _rbp

_rbp:
  mov rax, rbp
  ret;

这是不允许的...

无法汇编的唯一原因是因为您试图将 .text:声明为标签,而不是使用 .text 指令.如果删除尾随的:,它将与clang一起汇编(将 .intel_syntax .intel_syntax noprefix 相同).

The only reason that doesn't assemble is because you tried to declare .text: as a label, instead of using the .text directive. If you remove the trailing : it does assemble with clang (which treats .intel_syntax the same as .intel_syntax noprefix).

对于GCC/GAS进行组装,您还需要 noprefix 告诉它,寄存器名称不带前缀.(是的,您可以 具有Intel op dst,src顺序,但仍然具有%rsp 寄存器名称.不,您不应该这样做!)当然,GNU/Linux不使用前划线.

For GCC / GAS to assemble it, you'd also need the noprefix to tell it that register names aren't prefixed by %. (Yes you can have Intel op dst, src order but still with %rsp register names. No you shouldn't do this!) And of course GNU/Linux doesn't use leading underscores.

但是,如果您调用它,它并不总是会做您想做的事!如果您在没有优化的情况下编译了 main (因此 -fno-omit-frame-pointer 生效),那么可以,您将获得一个指向返回值下方的堆栈插槽的指针地址.

Not that it would always do what you want if you called it, though! If you compiled main without optimization (so -fno-omit-frame-pointer was in effect), then yes you'd get a pointer to the stack slot below the return address.

您肯定会错误地使用该值.(* p)-4; 加载保存的RBP值( * p ),然后偏移四个8字节的空指针.(因为这就是C指针数学的工作方式; * p 的类型为 void * ,因为 p 的类型为 void ** ).

And you definitely use the value incorrectly. (*p)-4; loads the saved RBP value (*p) and then offsets by four 8-byte void-pointers. (Because that's how C pointer math works; *p has type void* because p has type void **).

我认为您正在尝试获取自己的返回地址并重新运行到达main的 call 指令(在main的调用方中),最终由于推入更多的返回地址而导致堆栈溢出.在GNU C中,使用 void * __builtin_return_address(0) 以获得自己的寄信人地址.

I think you're trying to get your own return address and re-run the call instruction (in main's caller) that reached main, eventually leading to a stack overflow from pushing more return addresses. In GNU C, use void * __builtin_return_address (0) to get your own return address.

x86 call rel32 指令为5个字节,但是调用main的 call 可能是间接调用,使用寄存器中的指针.因此它可能是2字节的 call *%rax 或3字节的 call *%r12 ,除非您分解了调用方,否则您将不知道.(我建议使用反调试器在反汇编模式下在 main 的末尾按指令(GDB/LLDB stepi )单步执行.如果其中包含main的任何符号信息,呼叫者,您将可以向后滚动并查看上一条指令是什么.

x86 call rel32 instructions are 5 bytes, but the call that called main was probably an indirect call, using a pointer in a register. So it might be a 2-byte call *%rax or a 3-byte call *%r12, you don't know unless you disassemble your caller. (I'd suggest single-stepping by instructions (GDB / LLDB stepi) off the end of main using a debugger in disassembly mode. If it has any symbol info for main's caller, you'll be able to scroll backward and see what the previous instruction was.

如果没有,您可能必须尝试看看看起来是否理智.x86机器代码是可变长度的,因此不能明确地向后解码.您无法分辨指令(如立即数或ModRM)中的字节与指令开始之间的差异.这完全取决于您开始从何处拆卸.如果您尝试一些字节偏移,通常只有一个会产生看起来很健全的东西.

If not, you might have to try and see what looks sane; x86 machine code can't be unambiguously decoded backwards because it's variable-length. You can't tell the difference between a byte within an instruction (like an immediate or ModRM) vs. the start of an instruction. It all depends on where you start disassembling from. If you try a few byte offsets, usually only one will produce anything that looks sane.

   asm("movq %rax, 0"); //Exit code is 11, so now it should be 0

这是使用AT& T语法存储到绝对地址 0 的RAX.当然,这是段错误.退出代码11来自SIGSEGV,它是信号11.(使用 kill -l <​​/code>查看信号编号).

This is a store of RAX to absolute address 0, in AT&T syntax. This of course segfaults. exit code 11 is from SIGSEGV, which is signal 11. (Use kill -l to see signal numbers).

也许您想要 mov $ 0,%eax .尽管这里仍然没有意义,但是您将通过函数指针进行调用.在调试模式下,编译器可能会将其加载到RAX中并增加您的值.

Perhaps you wanted mov $0, %eax. Although that's still pointless here, you're about to call through your function pointer. In debug mode, the compiler might load it into RAX and step on your value.

此外,如果您不告诉编译器您要修改的寄存器(使用约束),则在 asm 语句中写入寄存器永远是不安全的.

Also, writing a register in an asm statement is never safe when you don't tell the compiler which registers you're modifying (using constraints).

   printf("Main: %p\n", main);
   printf("&Main: %p\n", &main); //WTF

main & main 是同一回事,因为 main 是一个函数.这就是C语法用于函数名称的方式. main 不是可以获取其地址的对象.&函数指针分配中的运算符可选

main and &main are the same thing because main is a function. That's just how C syntax works for function names. main isn't an object that can have its address taken. & operator optional in function pointer assignment

与数组相似:数组的裸名可以分配给指针,也可以作为指针arg传递给函数.但是& array 也是相同的指针,与& array [0] 相同.这仅适用于 array (如 int array [10] ),不适用于诸如 int * ptr 的指针;在后一种情况下,指针对象本身具有存储空间,并且可以拥有自己的地址.

It's similar for arrays: the bare name of an array can be assigned to a pointer or passed to functions as a pointer arg. But &array is also the same pointer, same as &array[0]. This is true only for arrays like int array[10], not for pointers like int *ptr; in the latter case the pointer object itself has storage space and can have its own address taken.

这篇关于为什么允许我使用ret退出main?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆