据我了解,当我根据GCC调用约定调用函数时,会发生以下情况:
呼叫者保存 AX、CX 和 DX 寄存器的值。参数和返回地址在堆栈上推送。此外,calle 必须保留 SI、DI、BX 和 BP 寄存器的值。
但是,状态注册呢?谁保存了它?
另外,推送在堆栈上的返回地址的值实际上是指令寄存器的值吗?
状态寄存器不会在函数调用之间保留。如果状态寄存器中有重要内容,则需要将其复制到其他地方(通常使用 SETcc),但调用约定不需要调用函数执行此操作,就像它不需要调用函数保存和恢复 AX 等人一样,如果它们中没有什么重要的东西。
回答你的第二个问题:
另外,推送在堆栈上的返回地址的值实际上是指令寄存器的值吗?
你的意思是call
指令推动的价值?是的,这是内部执行期间call
当前rip
(eip/ip
32/16 位模式)值(rip
指向下一条指令)。
并且ret
指令将弹出堆栈顶部的任何值,并将其设置为rip
,更改下一条指令的代码执行流(从ret
之后的下一个指令更改为堆栈中的地址/值)。因此,堆栈中的值在ret
完成后成为ip
寄存器的内容。ret
就像(不存在的)pop ip
,但它有自己的助记符,使其在人类阅读时更好地在源代码中脱颖而出,而且它具有完全不同的操作码,因此晶体管中的硬件实现完全特定于它(这在现代 x86 上是有意义的,其中ret
实现使用许多其他技巧以获得更好的性能, 但我有点好奇为什么 8086 不会将其编码为pop ip
,就像另一个pop
寄存器一样,即使在当时也可能在某些细节上有些特别)。
GCC 调用约定
GCC 在其目标平台上使用标准调用约定。 听起来你是在描述Linux上使用的i386 System V调用约定/ABI,和/或一些Windows调用约定。 (其中一些以不同的方式传递参数,但对可以破坏的寄存器做出相同的选择)。
您使用的是 16 位寄存器名称,但 gcc 几乎不支持 16 位 x86。 它基本上生成 32 位代码,然后将其与.code16
组装在一起,因此大多数指令都具有操作数大小和/或地址大小前缀。
呼叫者保存 AX、CX 和 DX 寄存器的值
不,仅当调用方包含要在整个call
中保留的任何数据时,调用方才会这样做。 正常情况是调用方让这些值死亡。 "调用方保存"与"被调用方保存"是一个不好的术语,因为它意味着所有寄存器实际上都保存在某个地方。
更容易理解,IMO,是
- 呼叫破坏:EAX ECX EDX 和条件代码(EFLAGS 的一部分),所有 XMM 注册
- 呼叫保留:EBX,ESI EDI,EBP,ESP。
DF 在调用和返回时必须为 0,因此字符串指令向上。 (DF是EFLAGS中的另一个位)。 x87 堆栈在call
和ret
上必须为空,但返回 FP 值的函数除外(在这种情况下,st0
具有返回值,x87 堆栈的其余部分为空)。
呼叫破坏意味着在call
之后,呼叫者必须假设寄存器包含垃圾,无论被叫方是否实际使用过寄存器。如果该寄存器中有任何调用者稍后需要的内容,则必须将其移动到其他地方。 但如果不是,让价值消亡是完全可以的。 例如,为了编译类似rv = foo(a + b + c)
的东西,调用者会在寄存器中计算a+b+c
。 但是,如果它在函数调用后不需要该值,则无需保留它。
呼叫保留意味着调用方可以假定寄存器值未更改,无论被叫方只是避免触摸该寄存器,还是被叫方保存/恢复了该寄存器。 (或者对于 ESP,被调用方通常会使用add esp, 28
或类似方式恢复它,以撤消它使用push
和sub
所做的任何更改。 被调用方如何设法返回仍然保存调用方值的调用保留寄存器并不重要,重要的是它确实如此。 这就是为什么"被调用方保存"也不是最明确的术语:它意味着被调用方明确地保存了它们。
但是,状态注册呢?谁保存了它?
没有人保存它,除非在极少数情况下。 如果需要,调用者可以保存它,但通常重做比较要容易得多,也便宜得多(popf
很慢,而且首先保存 EFLAGSpushf
不是免费的)。
或者更常见的是,条件代码中没有任何有用的数据,只有整数寄存器中的整数值。 大多数指令都会编写 EFLAG,但大多数时候您从未阅读过这些结果。 通常对整数结果使用add
、imul
等,忽略标志结果。
有趣的事实:64位OS X系统调用将CF设置为错误,否则它们会清除CF。 没有常见的 32 位或 64 位函数调用约定在 EFLAGS 中返回任何内容;他们只是被破坏了。 (对于 Linux 系统调用,将保留 EFLAGS/RFLAGS。 系统调用通常不会破坏除返回值以外的任何寄存器,部分原因是这样可以避免将内核信息泄漏回用户空间。