【问题标题】:Why do I get "address boundary error" from this piece of assembly code为什么我从这段汇编代码中得到“地址边界错误”
【发布时间】:2016-08-13 03:16:38
【问题描述】:

代码由我正在研究的编译器生成。但是我不知道为什么它会给我这个错误。没有注册冲突。当我尝试调用 callq *%rbx 时,我的程序立即崩溃,并收到一条错误消息“由信号 SIGSEGV 终止(地址边界错误)

我了解该程序似乎试图访问非法内存,但我不知道哪里出错了。谁能给我一个提示?

编辑:完成的汇编代码是here,我的编译器试图编译的代码是here。 runtime.c 是here

    .globl _main
_main:
    pushq   %rbp
    movq    %rsp, %rbp
    pushq   %r12
    pushq   %rbx
    pushq   %r14
    pushq   %r13
    subq    $0, %rsp
    movq    $16384, %rdi
    movq    $16, %rsi
    callq   _initialize
    movq    _rootstack_begin(%rip), %r15

    leaq    o13352(%rip),   %rbx
    leaq    z13351(%rip),   %rcx
    movq    $1, %rdi
    callq   *%rcx              <------ As noted in comments: z13351() clobbers %rbx
    movq    %rax,   %rcx
    movq    $20,    %rdi
    movq    %rcx,   %rsi
    callq   *%rbx       <-------------      (Address boundary error)

    ... 

【问题讨论】:

  • 首先,你为什么使用寄存器间接call指令而不是the usual call rel32编码?您是否重用了编译器在为全局变量发出加载/存储时使用的相同代码?
  • 当您的call 发生时,%rbx 中有什么内容?之前的函数调用是否可能会破坏%rbx(违反调用约定,至少如果您使用的是标准函数之一)。它是可执行内存的地址吗?如果这是一个 JIT 编译器,你还记得确保内存是可执行的吗?在某些系统上,malloc 和静态分配默认不是来自可执行页面。如果这不是问题,您是否为 RIP 相关的lea 正确编码?
  • @PeterCordes 嗨,在我的编译器中,我用function-ref 标签注释了所有函数,以便我可以将它们与局部变量区分开来。对不起,我不太明白你的第二个问题。你指的是什么全局变量?
  • 我试图猜测为什么你的编译器会生成如此愚蠢的代码,而不是发出call o15954。在将 RIP 相对地址加载到寄存器后,寄存器间接call 完全是脑死(并且浪费了分支预测资源)。显然,您的编译器仍处于非常早期的阶段,因此可以预期会出现脑死亡代码,但值得指出。 (请参阅x86 tag wiki 中的性能链接,尤其是 Agner Fog 的东西。)但无论如何,您将需要使用调试器。这是必不可少的。
  • 众所周知,间接分支不利于性能。它还浪费了一个寄存器。代码大小也更糟,并且需要更多指令。有效的替代方法是通过标签调用函数的常规方法是使用call o15954,因此与您在未优化的 gcc 输出中看到的代码相比,编译器发出的代码过于复杂。玩具编译器生成糟糕的代码是可以的,但认识到这一事实是个好主意。我不是说你笨,只是你的编译器发出的 asm。

标签: assembly compiler-construction x86 x86-64


【解决方案1】:

您的代码出现段错误,因为%rbx 在第一个函数返回后不再指向正确的位置。因此,您无需调用o13352,而是跳转到某个未映射的地址。在无效地址上获取代码并加载/存储 SIGSEGV


您可能需要修复 %rbx 破坏,但这是一个单独的问题。对此的最佳答案是使用直接call insns 发出对命名符号的调用,而不是使用相对 RIP 的 LEA 和间接 CALL 的复杂函数调用序列。

...               # set up args
call    z13351
...               # set up args
call    o13352

编译器生成更简单,人类更容易理解(有助于调试其他问题),并且显着提高代码效率:不浪费寄存器,更小代码大小,指令更少,并避免间接分支的分支错误预测(您应该将其用于跳转表的函数指针,但不能用于此)。

请参阅 标签 wiki 中的链接以获取更多性能和其他内容。尤其是Agner Fog's guides 是一个很好的资源,可以帮助您编写创建好的和正确的 asm。当然,玩具编译器并不期望生成好的代码,但了解什么是好什么是坏是个好主意。 Agner Fog 的指南可能还会帮助您更好地理解事物,这将有助于生成正确的代码,无论效率如何。


如果你想通过函数指针继续使用间接调用,即使在不必要的时候:

根据您的 cmets,z15953 不会保留 %rbx。 (或z13351 或在此代码更新中调用的任何名称)。假设这是一个错误,修复它也将解决这个问题,因为您问题中的代码看起来正确(但令人讨厌)。

您没有指定编译器尝试为什么 ABI 生成代码,但我认为这是一个调用约定,其中 %rbx 应该是调用保留的(又名被调用者保存,又名非易失性)。因此,即使您将函数调用序列更改为使用正常的直接调用,也可能出于其他原因需要修复该错误。

看起来您在生成的函数中推送/弹出%rbx 以保存/恢复它。将其简化为仍能证明此问题的尽可能简单的案例。您在问题中链接到的代码太大,甚至无法包含在内,所以它显然甚至不接近 Minimal, Complete, and Verifiable example (不,我对涉水这个烂摊子不感兴趣,对不起。你写了你的编译器,所以你可能比我更容易识别臃肿代码的模式并找到可能相关的部分。)

由于您的函数仍然成功地设法ret,这意味着您没有破坏堆栈(即沿着不包括结尾pops 的路径到达ret)。但也许你覆盖了堆栈上%rbx 的保存值?在使用 rsp 相对地址存储临时文件之前,您是否通过 sub $size, %rsp 预留了足够的空间?


愚蠢的创可贴修复:

调度call之后第二个地址的lea

# your original:
leaq    o13352(%rip),   %rbx
leaq    z13351(%rip),   %rcx
...
callq   *%rcx
...                             # set up args
callq   *%rbx

# what you should do instead:
...
leaq    z13351(%rip),   %rcx
callq   *%rcx
...                             # set up args
leaq    o13352(%rip),   %rcx
callq   *%rcx

这通过重复使用相同的寄存器来设置函数指针来节省一个寄存器,而不是为您要调用的每个函数使用不同的寄存器。 (这显然不能扩展到调用许多其他函数的函数。)

【讨论】:

  • 嗨。感谢您的帮助。当我完成我计划实现的所有功能并通过所有测试时,我将更改为直接callinstr,因为测试代码由我正在学习的课程提供。测试代码(解释器)假设我使用间接调用。
猜你喜欢
  • 1970-01-01
  • 2014-01-22
  • 2016-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-26
  • 1970-01-01
相关资源
最近更新 更多