如果我说'打电话'而不是跳?会怎么样?由于没有写入返回语句,控制权只是转移到下面的下一行,还是在调用后仍然返回到该行?
start:
mov $0, %eax
jmp two
one:
mov $1, %eax
two:
cmp %eax, $1
call one
mov $10, %eax
Run Code Online (Sandbox Code Playgroud)
除非分支指令在其他地方发送执行,否则CPU总是在内存中执行下一条指令.
标签没有宽度或对执行有任何影响.它们只允许您从其他地方引用此地址. 执行只是通过标签或功能结束.
如果省略ret函数的末尾,执行会继续执行并解码下一步作为指令.(通常情况下,如果系统执行零填充文件的一部分,会发生什么情况?如果这是asm源文件中的最后一个函数)
您可以(也许应该)在调试器中自己尝试.单步执行该代码并观察RSP和RIP的变化.关于asm的好处是CPU的总状态(不包括内存内容)不是很大,所以可以在调试器窗口中观察整个架构状态.(好吧,至少是与用户空间整数代码相关的有趣部分,因此排除了只有OS可以调整的模型特定寄存器,并且不包括FPU和向量寄存器.)
call并且ret不是"特殊的"(即CPU不"记住"它在"功能"内部).
它们只是完成了手册所说的内容,并且由您自己正确使用它们来实现函数调用和返回.(例如,确保堆栈指针在ret运行时指向返回地址.)还可以由您来调整调用约定,以及所有这些内容.(参见x86标签wiki.)
还有什么特别的一个标签,你jmp要对一个你的标签call.汇编程序只是将字节汇编到输出文件中,并记住放置标签标记的位置.它并不像C编译器那样真正"了解"函数.您可以在任何地方放置标签,但不会影响机器代码字节.
使用该.globl one指令会告诉汇编器在符号表中放入一个条目,以便链接器可以看到它.这将允许您定义一个可以从其他文件中使用的标签,甚至可以从C调用.但这只是目标文件中的元数据,并且仍然不会在指令之间放置任何内容.
如果call使用等效push的返回地址进行模拟,然后使用a,则代码将以完全相同的方式运行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.
Run Code Online (Sandbox Code Playgroud)
请注意,此序列仅适用于非PIC代码,因为绝对返回地址已编码到push imm32指令中.在具有备用寄存器的64位代码中,您可以使用RIP相对lea来将返回地址放入寄存器并在跳转之前将其推送.
还要注意,虽然在体系结构上CPU不会"记住"过去的CALL指令,但是假设调用/ ret对将匹配,实际实现运行得更快,并使用返回地址预测器来避免ret上的错误预测.
为什么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);
}
Run Code Online (Sandbox Code Playgroud)
# asm output from clang:
foo:
add edi, edi
jmp ext_func # TAILCALL
Run Code Online (Sandbox Code Playgroud)
将ret在年底ext_func将返回foo的调用者. foo可以使用此优化,因为它不需要对返回值进行任何修改或进行任何其他清理.
在SystemV x86-64调用约定中,第一个整数arg在edi.所以这个函数用a +替换它,然后跳转到开头ext_func.在进入时ext_func,一切都处于正确状态,就像运行时一样call ext_func.堆栈指针指向返回地址,args是它们应该的位置.
尾调用优化可以在register-args调用约定中进行,而不是在堆栈中传递args的32位调用约定中进行.您经常遇到问题的情况,因为您想要尾调用的函数比当前函数需要更多的参数,因此没有空间将我们自己的args重写为函数的args.(并且编译器不会创建修改自己的args的代码,即使ABI非常清楚函数拥有堆栈空间来保存它们的args并且如果需要可以破坏它.)
在一个调用约定中,被调用者清理堆栈(ret 8在返回地址后用另外8个字节弹出或者某个东西),你只能尾调用一个带有完全相同arg字节数的函数.
您的直觉是正确的:函数返回后,控制权会传递到下面的下一行。
在您的情况下, after call one,您的函数将跳转到mov $1, %eax,然后继续向下cmp %eax, $1,并最终陷入无限循环,就像您call one再次所做的那样。
除了无限循环之外,您的函数最终将超出其内存限制,因为call命令将当前rip(指令指针)写入堆栈。最终,你会溢出堆栈。
| 归档时间: |
|
| 查看次数: |
1331 次 |
| 最近记录: |