如果汇编程序中的 CALLed 代码块中没有 return 语句,该怎么办

What if there is no return statement in a CALLed block of code in assembly programs

提问人:tanvi 提问时间:12/18/2016 最后编辑:Peter Cordestanvi 更新时间:9/12/2021 访问量:4739

问:

如果我说“呼叫”而不是跳转会怎样?由于没有编写 return 语句,控制权是直接传递到下面的下一行,还是在调用后仍然返回到该行?

start:
     mov $0, %eax
     jmp two
one:
     mov $1, %eax
two:
     cmp %eax, $1
     call one
     mov $10, %eax
组件 x86

评论

3赞 Michael Petch 12/18/2016
是的,当你执行的下一个语句之后将是,然后它会再次。这将一直持续到堆栈内存不足,并且可能会使操作系统出现故障。如果要结束函数,请使用 .之后将从函数返回并继续在线call onemov $1, eaxcmp %eax, $1call oneretretmov $1, eaxonemov $10, %eax
0赞 Michael Petch 12/18/2016
我应该说“错误过程”,而不是操作系统。
0赞 Cody Gray - on strike 12/18/2016
参见:stackoverflow.com/questions/28964692/...stackoverflow.com/questions/36568642/...stackoverflow.com/questions/7060970/...stackoverflow.com/questions/32793117/assembly-call-vs-jmp
0赞 tanvi 1/3/2017
谢谢迈克尔,这为我清除了:)

答:

5赞 gowrath 12/18/2016 #1

您的直觉是正确的:在函数返回后,控件会传递到下面的下一行。

在你的例子中,之后,你的函数将跳到,然后继续向下,并最终进入一个无限循环,就像你再次一样。call onemov $1, %eaxcmp %eax, $1call one

除了无限循环之外,您的函数最终将超越其内存限制,因为命令将当前(指令指针)写入堆栈。最终,您将溢出堆栈。callrip

11赞 Peter Cordes 12/18/2016 #2

CPU 始终在内存中执行下一条指令,除非分支指令将执行发送到其他地方。

标签没有宽度,也不对执行有任何影响。他们只是允许您从其他地方引用此地址。执行只是通过标签,如果你不避免这一点,甚至在代码的末尾

如果您熟悉 C 或其他语言,例如,您用来标记地点的标签可以与 asm 标签完全相同,并且/与 or 完全相同。但是 asm 没有特殊的函数语法;你必须自己实现这个高级概念。gotogotojmpjccgotoif(EFLAGS_condition) goto

如果你省略了代码块的末尾,执行会继续执行,并将接下来的任何内容解码为指令。(如果系统执行文件的一部分是零填充的,会发生什么?如果这是 ASM 源文件中的最后一个函数,或者执行可能落入最终返回的某个 CRT 启动函数。ret

(在这种情况下,你可以说你所说的块不是一个函数,只是一个函数的一部分,除非它是一个错误和一个 or 是有意的。retjmp


您可以(也许应该)在调试器中自己尝试此操作。单步完成该代码,观察 RSP 和 RIP 的变化。asm 的好处是 CPU 的总状态(不包括内存内容)不是很大,因此可以在调试器窗口中观察整个架构状态。(好吧,至少是与用户空间整数代码相关的有趣部分,因此排除了只有操作系统可以调整的特定于模型的寄存器,并排除了 FPU 和矢量寄存器。


callret 并不“特殊”(即 CPU 不会“记住”它在“函数”中)。

它们只是完全按照手册所说的去做,由你来正确使用它们来实现函数调用和返回。(例如,确保堆栈指针在运行时指向返回地址。你也要确保调用约定正确,以及所有这些东西。(请参阅 标记 wiki。ret

你去的标签和你去的标签也没有什么特别之处。汇编程序只是将字节组装到输出文件中,并记住标签标记的放置位置。它并不像 C 编译器那样真正“了解”函数。您可以将标签放在任何您想要的位置,并且不会影响机器代码字节。jmpcall

使用该指令将告诉汇编程序在符号表中放置一个条目,以便链接器可以看到它。这样一来,你就可以定义一个标签,这个标签可以从其他文件使用,甚至可以从C语言调用。但这只是目标文件中的元数据,仍然没有在指令之间放置任何内容。.globl one

标签只是 asm 中可用于实现“函数”(又名过程或子例程)的高级概念的机制的一部分:调用者调用的标签,以及最终会以一种或另一种方式跳回调用者传递的返回地址的代码。但并非每个标签都是函数的开始。有些只是循环的顶部,或者是函数中条件分支的其他目标。


如果您模拟调用,则使用等效的返回地址推送,然后是 jmp,则代码的运行方式将完全相同。

one:
     mov   $1, %eax
     # missing ret  so we fall through
two:
     cmp   %eax, $1
     # call one               # emulate it instead with push+jmp
     pushl  $.Lreturn_address
     jmp   one
.Lreturn_address:
     mov   $10, %eax
     # fall off into whatever comes next, if it ever reaches here.

请注意,此序列仅在非 PIC 代码中起作用,因为绝对返回地址被编码到推送 imm32 指令中。在具有备用寄存器的 64 位代码中,您可以使用 RIP 相对将返回地址输入寄存器,并在跳转之前推送该地址。lea


另请注意,虽然从架构上讲,CPU 不会“记住”过去的 CALL 指令,但通过假设 call/ret 对将被匹配,并使用返回地址预测器来避免对 ret 的错误预测,实际实现运行得更快。

为什么RET很难预测?因为它是间接跳转到存储在内存中的地址!它等价于 / ,因此,如果您有一个备用寄存器来 clobber,则可以以这种方式模拟它(例如,rcx 在大多数调用约定中不保留调用,并且不用于返回值)。或者,如果您有一个红色区域,因此堆栈指针下方的值仍然可以避免被异步破坏(通过信号处理程序或其他方式),则可以 / .pop %internal_tmpjmp *%internal_tmpadd $8, %rspjmp *-8(%rsp)

显然,对于实际使用,您应该使用 ,因为这是最有效的方法。我只是想使用多个更简单的指令指出它的作用。仅此而已。ret


请注意,函数可以以 tail-call 而不是 ret

(在 Godbolt 上看到这个)

int ext_func(int a);  // something that the optimizer can't inline

int foo(int a) {
  return ext_func(a+a);
}
# asm output from clang:

foo:
    add     edi, edi
    jmp     ext_func                # TAILCALL

末尾将返回给 的调用方。 可以使用此优化,因为它不需要对返回值进行任何修改或执行任何其他清理。retext_funcfoofoo

在 SystemV x86-64 调用约定中,第一个整数 arg 位于 中。所以这个函数用 a+a 替换它,然后跳到 的开头。在进入 时,一切都处于正确的状态,就像某些东西运行了一样。堆栈指针指向返回地址,而参数位于它们应该在的位置。ediext_funcext_funccall ext_func

尾调用优化可以在寄存器参数调用约定中比在堆栈上传递参数的 32 位调用约定更频繁地完成。你经常会遇到这样的情况,因为你要尾随调用的函数比当前函数需要更多的参数,所以没有空间将我们自己的参数重写为函数的参数。(编译器不倾向于创建修改自己的参数的代码,尽管 ABI 非常清楚函数拥有保存其参数的堆栈空间,并且可以根据需要对其进行破坏。

在调用约定中,被调用方清理堆栈(在返回地址后再弹出 8 个字节),您只能尾随调用采用完全相同数量的 arg 字节的函数。ret 8

评论

0赞 Jongware 12/18/2016
相反(我认为)是使 x86 代码地址独立于此的构造:.这是一个“什么都不做”的调用,也就是说,它也永远不会返回,因为你偷了它的返回地址。它恰好靠近您自己的地址(正负几个字节)。call $+5; pop eax
1赞 Peter Cordes 12/19/2016
@Cody:感谢您对此以及其他一些最近的答案进行清理编辑:)
0赞 Peter Cordes 10/22/2020
是的,/ 是 clang 用于 32 位 PIC 代码的代码。64 位代码只能使用相对于 RIP 的 LEA。你可能会认为不匹配的调用(没有 ret)会弄乱现代 CPU 中的返回地址预测器,但它们是 rel32=0 的特殊情况。blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0/直接读取程序计数器。(删除了我之前的回复,认为这将是一个问题。call +0pop eax