为什么我可以使用 ret 退出 main?

2024-05-01

我即将弄清楚程序堆栈到底是如何设置的。 我了解到用以下方式调用该函数

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: %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

...

.intel_syntax

.text:

.global _rbp

_rbp:
  mov rax, rbp
  ret;

不出所料,这是不允许的,也许是因为此时的指令不完全是 64 位,也许是因为 UNIX 不允许这样做......

But also不允许此调用:

   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 系统如何有效地检查此类尝试...... 我希望我的问题很清楚......

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


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。但无论如何,静态链接在那些.oCRT 文件包含_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;在后一种情况下,指针对象本身具有存储空间并且可以获取其自己的地址。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么我可以使用 ret 退出 main? 的相关文章

  • 高级 Win32 图像文件 I/O?

    我想在 Windows C 应用程序中将图像文件读入内存 什么是一个相当简单的解决方案 也许类似于 IOS 提供的UIImage 我希望支持合理数量的文件格式 我需要为图像处理的位图提供一些低级访问权限 我在互联网上阅读了很多内容 看起来
  • LINQ to XML - 如何正确使用 XDocument

    现在我首先要说的是 这确实是一项任务 然而 在我遇到 Linq to XML 语法之前 我几乎已经完成了它 我有 2 个课程 曲目和 CD 现在作为作业的一部分 我创建了一张 CD 然后向其中添加了一些曲目 在搜索了大量完美解释了如何从 x
  • 将 2D 数组映射到 1D 数组

    我想用一维数组来表示一个二维数组 函数将传递两个索引 x y 和要存储的值 这两个索引代表一维数组的单个元素 并相应地设置它 我知道一维数组需要具有 arrayWidth arrayHeight 的大小 但我不知道如何设置每个元素 例如 如
  • 如何在 C# 事件中区分更改是由代码还是由用户进行?

    我有一个简单的TextBox一开始是空的 我有一个简单的事件 TextChanged 可以知道用户何时更改了其中的任何内容TextBox 但是 如果我自己在代码中对其执行任何操作 该事件就会触发 喜欢设置textbox Text Test
  • 处理 LINQ sum 表达式中的 null

    我正在使用 LINQ 查询来查找列的总和 并且在少数情况下该值有可能为空 我现在使用的查询是 int score dbContext domainmaps Where p gt p SchoolId schoolid Sum v gt v
  • 在 Windows Phone 上启动 pdf 文件时出现 System.Runtime.InteropServices.COMException

    我正在尝试使用我之前在另一个应用程序上使用过的以下工作代码打开 pdf 文件 但这一次 当流程到达此行时 我收到 System Runtime InteropServices COMException Windows System Laun
  • 对数字进行向上和向下舍入 C++

    我试图让我的程序分别向上和向下舍入数字 例如 如果数字是3 6 我的程序应该四舍五入最接近的数字 4 如果该数字是3 4 它将向下舍入为 3 我尝试使用ceil库获取 3 个项目的平均值 results ceil marks1 marks2
  • glDrawElements 只绘制半个四边形

    这是我的功能 void Object draw2 if mIsInitialised return Tell OpenGL about our vertex and normal data glEnableClientState GL VE
  • 如何检测斑点并将其裁剪成 png 文件?

    我一直在开发一个网络应用程序 我陷入了一个有问题的问题 我会尝试解释我想要做什么 在这里您看到第一个大图像 其中有绿色形状 我想要做的是将这些形状裁剪成不同的 png 文件 并使它们的背景透明 就像大图像下面的示例裁剪图像一样 第一张图像将
  • 如何在不使用reinterpret_cast的情况下使用dlsym()加载函数?

    我正在尝试使用 clang tidy 来强制执行 C 核心指南 虽然它确实有很多有效点 但有一件事我无法真正解决 dlsym 返回一个void 我需要以某种方式将其转换为正确的函数指针 为此 我使用reinterpret cast 由于指南
  • 当格式字符串包含“{”时,String.Format 异常

    我正在使用 VSTS 2008 C Net 2 0 执行以下语句时 String Format 语句抛出 FormatException 有什么想法是错误的吗 这是获取我正在使用的 template html 的位置 我想在 templat
  • 在生产者-消费者情况下使用条件变量

    我正在尝试了解条件变量以及如何在生产者 消费者情况下使用它 我有一个队列 其中一个线程将数字推入队列 而另一个线程从队列中弹出数字 当生产线程放置一些数据时 我想使用条件变量向消费线程发出信号 问题是有时 或大多数时候 它只将最多两个项目推
  • 抽象类和接口之间的区别[重复]

    这个问题在这里已经有答案了 可能的重复 接口与基类 https stackoverflow com questions 56867 interface vs base class 我不明白抽象类和接口之间的区别 我什么时候需要使用哪种字体
  • 在可观察项目生成时对其进行处理

    我有一个IObservable它会生成一次性物品 并且在其生命周期内可能会生成无限数量的物品 因此 我想在每次生成新项目时处理最后一个项目 因此Using http reactivex io documentation operators
  • 使用 Linq 进行异步Where过滤

    我有一个List通过填充的元素async调用 WebService 没问题 我需要过滤该列表以便在应用程序视图上显示某些内容 我试过这个 List
  • 使用 AutoMapper 进行 LINQ GroupBy 聚合

    试图让查询工作 但老实说不确定如何 或者是否可能 进行它 因为我尝试过的一切都不起作用 共查询6个表 Person PersonVote PersonCategory Category City FirstAdminDivision Per
  • 编写专门用于类及其子类的函数模板

    我正在尝试编写一个函数模板 一个版本应该用于不满足另一版本标准的所有类型 当参数是给定类的基类或该类本身时 应使用另一个版本 我尝试过超载Base 但是当类派生自Base 他们使用通用的 而不是特定的 我也尝试过这种 SFINAE 方法 s
  • OpenMP C 程序运行速度比顺序代码慢

    我是 OpenMP 的新手 正在尝试并行化 Jarvis 的算法 然而事实证明 与顺序代码相比 并行程序花费的时间要长 2 3 倍 难道问题本身就不能并行化吗 或者我并行化它的方式有问题 这是我针对该问题的 openMP 程序 其中有 2
  • 改进C++逐行读取文件的能力?

    我正在解析大约 500GB 的日志文件 我的 C 版本需要 3 5 分钟 我的 Go 版本需要 1 2 分钟 我正在使用 C 的流来流式传输文件的每一行以进行解析 include
  • SSBO 是更大的 UBO?

    我目前正在 OpenGL 4 3 中使用 UBO 进行渲染 以将所有常量数据存储在 GPU 上 诸如材料描述 矩阵等内容 它可以工作 但是 UBO 的小尺寸 我的实现为 64kB 迫使我多次切换缓冲区 减慢渲染速度 我正在寻找类似的方法来存

随机推荐