如果我说“打电话”会怎样?而不是跳?由于没有写入返回语句,控件是否只是转到下面的下一行,还是在调用后仍然返回到该行?
start:
mov $0, %eax
jmp two
one:
mov $1, %eax
two:
cmp %eax, $1
call one
mov $10, %eax
答案 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和向量寄存器。)
call
和ret
不是"特别" (即,CPU没有记住"它是在"功能")内。
他们只是完成了手册所说的内容,并且您可以正确使用它们来实现函数调用和返回。 (例如,确保当ret
运行时,堆栈指针指向返回地址。)还可以由您来确定调用约定,以及所有这些内容。 (参见x86标签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
结束:
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
(指令指针)写入堆栈,因此您的函数最终将超出其内存约束。最终,你会溢出堆栈。