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

时间:2016-12-18 02:56:49

标签: assembly x86

如果我说“打电话”会怎样?而不是跳?由于没有写入返回语句,控件是否只是转到下面的下一行,还是在调用后仍然返回到该行?

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

2 个答案:

答案 0 :(得分:7)

CPU总是在内存中执行下一条指令,除非分支指令在其他地方执行执行。

标签没有宽度或对执行产生任何影响。它们只允许您从其他地方引用此地址。 执行只会通过标签或功能结束。

如果在函数末尾省略ret,执行会继续执行并解码下一步作为指令。 (通常What would happen if a system executes a part of the file that is zero-padded?如果那是asm源文件中的最后一个函数)

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

callret不是"特别" (即,CPU没有记住"它是在"功能")内。

他们只是完成了手册所说的内容,并且您可以正确使用它们来实现函数调用和返回。 (例如,确保当ret运行时,堆栈指针指向返回地址。)还可以由您来确定调用约定,以及所有这些内容。 (参见标签wiki。)

对于jmp与您call标签的标签,没有什么特别之处。汇编程序只是将字节汇编到输出文件中,并记住放置标签标记的位置。它并不真正"知道"关于C编译器的功能。您可以在任何地方放置标签,它不会影响机器代码字节。

使用.globl one指令会告诉汇编程序在符号表中放入一个条目,以便链接器可以看到它。这样您就可以定义一个可以从其他文件中使用的标签,甚至可以从C调用。但这只是目标文件中的元数据,并且仍然没有在指令之间放置任何东西。 / p>

如果您使用等效call的返回地址,然后push模拟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代码,因为绝对返回地址已编码到push imm32指令中。在具有备用寄存器的64位代码中,您可以使用RIP相对lea将返回地址放入寄存器并在跳转之前将其推送。

另请注意,虽然在架构上CPU并没有记住"过去的CALL指令,假设调用/ ret对将匹配,实际实现运行得更快,use a return-address predictor以避免错误预测。

为什么RET难以预测?因为它间接跳转到存储在内存中的地址!它相当于pop %internal_tmp / jmp *%internal_tmp,所以如果你有一个备用寄存器来破解它就可以这样模仿它(例如rcx在大多数调用约定中没有被调用保存,并且没有被使用对于返回值)。或者,如果你有一个红区,那么堆栈指针下方的值仍然可以安全地被异步破坏(通过信号处理程序或其他),你可以add $8, %rsp / jmp *-8(%rsp)

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

请注意,函数可以以尾调用而不是ret 结束:

(see this on 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

ret末尾的ext_func将返回foo 来电者foo可以使用此优化,因为它不需要对返回值进行任何修改或进行任何其他清理。

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

尾调用优化可以在register-args调用约定中比在32位调用约定中更频繁地完成,该约定在栈上传递args。你经常遇到问题的情况,因为你想要尾调用的函数比当前函数需要更多的args,因此没有空间将我们自己的args重写为函数的args。 (并且编译器不会创建修改自己的args的代码,即使ABI非常清楚函数拥有堆栈空间持有他们的args并且如果他们想要可以破坏它。)

在一个调用约定中,被调用者清理堆栈(使用ret 8或其他东西在返回地址后弹出另外8个字节),你只能尾调用一个带有完全相同arg字节数的函数

答案 1 :(得分:2)

你的直觉是正确的:控制只是在函数返回后传递给下面的下一行。

在您的情况下,在call one之后,您的函数将跳转到mov $1, %eax,然后继续向下cmp %eax, $1并最终进入无限循环,您将再次call one

除了无限循环之外,由于call命令将当前rip(指令指针)写入堆栈,因此您的函数最终将超出其内存约束。最终,你会溢出堆栈。