CPU 总是执行内存中的下一条指令,除非分支指令将执行发送到其他地方。
标签没有宽度,对执行没有任何影响。它们只是允许您从其他地方引用此地址。 执行只是通过标签,如果你不避免的话,甚至在你的代码末尾。
如果您熟悉 C 或其他具有 goto (example) 的语言,则用于标记位置的标签可以 goto 与 asm 标签完全相同,jmp / jcc 的工作方式与 goto 或 if(EFLAGS_condition) goto 完全相同。但是 asm 没有函数的特殊语法;你必须自己实现这个高级概念。
如果您在代码块末尾省略ret,则执行将继续执行并将接下来的任何内容解码为指令。 (如果这是 asm 源文件中的最后一个函数,则可能是 What would happen if a system executes a part of the file that is zero-padded?,或者执行落入最终返回的某个 CRT 启动函数。)
(在这种情况下,您可以说您正在谈论的块不是一个函数,只是一个函数的一部分,除非它是一个错误并且 ret 或 jmp 是打算。)
您可以(也许应该)在调试器中自己尝试一下。单步执行该代码并观察 RSP 和 RIP 变化。 asm 的好处是 CPU 的总状态(不包括内存内容)不是很大,因此可以在调试器窗口中查看整个架构状态。 (好吧,至少是与用户空间整数代码相关的有趣部分,因此不包括只有操作系统可以调整的特定于模型的寄存器,并且不包括 FPU 和向量寄存器。)
call 和 ret 不是“特殊”(即 CPU 不会“记住”它位于“函数”中)。
它们只是按照手册中的说明进行操作,您可以正确使用它们来实现函数调用和返回。 (例如,确保在 ret 运行时堆栈指针指向返回地址。)还取决于您是否让调用约定正确,以及所有这些东西。 (请参阅x86 标签 wiki。)
您jmp 的标签与您call 的标签也没有什么特别之处。汇编器只是将字节汇编到输出文件中,并记住您放置标签标记的位置。它并不像 C 编译器那样真正“了解”函数。您可以将标签放在您想要的任何位置,并且不会影响机器码字节。
使用.globl one 指令会告诉汇编器在符号表中放置一个条目,以便链接器可以看到它。这将允许您定义一个可从其他文件使用的标签,甚至可从 C 调用。但这只是目标文件中的元数据,仍然没有在指令之间放置任何内容。
标签只是您可以在 asm 中用于实现“函数”(也称为过程或子例程)的高级概念的机制的一部分:标签供调用者调用,以及最终以一种或另一种方式跳转回调用者传递的返回地址的代码。但并非每个标签都是函数的开始。有些只是循环的顶部,或函数内条件分支的其他目标。
如果您模拟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-relative lea 将返回地址放入寄存器并在跳转前将其推送。
另请注意,虽然 CPU 在架构上不会“记住”过去的 CALL 指令,但通过假设 call/ret 对将匹配,以及 use a return-address predictor 以避免对 ret 的错误预测,实际实现运行得更快。
为什么 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
ext_func 末尾的ret 将返回到foo 的调用者。 foo 可以使用此优化,因为它不需要对返回值进行任何修改或进行任何其他清理。
在 SystemV x86-64 调用约定中,第一个整数 arg 位于 edi 中。所以这个函数用 a+a 替换它,然后跳转到 ext_func 的开头。在进入ext_func 时,一切都处于正确状态,就像运行call ext_func 时一样。堆栈指针指向返回地址,而 args 是它们应该在的位置。
尾部调用优化可以在 register-args 调用约定中比在堆栈上传递 args 的 32 位调用约定中更频繁地完成。你经常会遇到问题,因为你想要尾调用的函数比当前函数需要更多的参数,所以没有空间将我们自己的参数重写为函数的参数。 (并且编译器不倾向于创建修改其自己的参数的代码,即使 ABI 非常清楚函数拥有保存其参数的堆栈空间并且可以根据需要破坏它。)
在被调用者清理堆栈的调用约定中(使用ret 8 或在返回地址后弹出另外 8 个字节的东西),您只能尾调用一个采用完全相同数量的 arg 字节的函数。