我一直在尝试找到 OCaml 调用约定,以便我可以手动解释 gdb 无法解析的堆栈跟踪。不幸的是,除了一般性观察之外,似乎没有用英语写过任何东西。例如,人们会在博客上评论 OCaml 在寄存器中传递了许多参数。(如果某处有英文文档,将不胜感激。)
所以我一直试图从 ocamlopt 源中解开它。谁能证实这些猜测的准确性?
而且,如果我对在寄存器中传递的前十个参数是正确的,那么通常不可能将参数恢复到函数调用吗?在 C 语言中,只要我回到正确的帧,参数仍然会被推入堆栈的某个地方。在 OCaml 中,被调用者似乎可以自由地销毁调用者的参数。
寄存器分配(来自/asmcomp/amd64/proc.ml
)
为了调用 OCaml 函数,
- 前 10 个整数和指针参数在寄存器 rax、rbx、rdi、rsi、rdx、rcx、r8、r9、r10 和 r11 中传递
- 前 10 个浮点参数在寄存器 xmm0 - xmm9 中传递
- 额外的参数被推入堆栈(最左边先入?),浮点数和整数以及混合的指针
- 陷阱指针(参见下面的异常)在 r14 中传递
- 在 r15 中传递分配指针(可能是本博文中所述的次要堆)
- 如果是整数或指针,返回值在 rax 中传回,如果是浮点数,返回值在 xmm0 中
- 所有寄存器都是调用者保存的?
为了调用 C 函数,使用标准 amd64 C 约定:
- 前六个整数和指针参数在 rdi、rsi、rdx、rcs、r8 和 r9 中传递
- 前八个浮点参数在 xmm0 - xmm7 中传递
- 额外的参数被压入堆栈
- 返回值在 rax 或 xmm0 中传回
- 寄存器 rbx、rbp 和 r12 - r15 是被调用者保存的
退货地址(来自/asmcomp/amd64/emit.mlp
)
根据 amd64 C 约定,返回地址是压入调用帧的第一个指针。(我猜该ret
指令假定这种布局。)
例外(来自/asmcomp/linearize.ml
)
代码try (...body...) with (...handler...); (...rest...)
像这样线性化:
Lsetuptrap .body
(...handler...)
Lbranch .join
Llabel .body
Lpushtrap
(...body...)
Lpoptrap
Llabel .join
(...rest...)
然后像这样作为组件发出(右侧的目的地):
call .body
(...handler...)
jmp .join
.body:
pushq %r14
movq %rsp, %r14
(...body...)
popq %r14
addq %rsp, 8
.join:
(...rest...)
在身体的某个地方,有一个线性化的操作码Lraise
,它作为这个精确的程序集发出:
movq %r14, %rsp
popq %r14
ret
这真的很整洁!我们创建了一个虚拟帧,而不是这个 setjmp/longjmp 业务,它的返回地址是异常处理程序,其唯一的本地是前一个这样的虚拟帧。有一条注释将/asmcomp/amd64/proc.ml
$r14 称为“陷阱指针”,因此我将此虚拟框架称为陷阱框架。当我们要引发异常时,我们将堆栈指针设置为最近的陷阱帧,将陷阱指针设置为之前的陷阱帧,然后“返回”到异常处理程序中。我敢打赌,如果异常处理程序不能处理这个异常,它只会重新引发它。
例外情况在 %eax 中。