【问题标题】:Pad instruction so end is aligned填充指令使末端对齐
【发布时间】:2024-04-19 21:00:02
【问题描述】:

我在 i386 上使用 GNU 汇编器,通常在 32 位 Linux 下(我也打算在 Cygwin 下寻找解决方案)。

我有一个“存根”函数:

    .align 4
stub:
    call *trampoline
    .align 4
stub2:

trampoline:
    ...

这个想法是 stub 和 stub2 之间的数据将与函数指针和一些上下文数据一起复制到分配的内存中。当内存被调用时,其中的第一条指令将压入下一条指令的地址并转到trampoline,它将从堆栈中读取地址并找出伴随数据的位置。

现在,stub 被编译为:

ff 15 44 00 00 00      call *0x44
66 90                  xchg %ax,%ax

这是对绝对地址的调用,这很好,因为call 的地址是未知的。填充已经变成了我猜是什么都不做的操作,这很好,无论如何它永远不会被执行,因为trampoline会在跳转到函数指针之前重写堆栈。

问题是这个调用推送的返回地址将指向未对齐的xchg 指令,而不是刚刚经过它的对齐数据。这意味着trampoline 需要更正对齐才能找到数据。这不是一个严重的问题,但最好生成以下内容:

66 90                  xchg %ax,%ax
ff 15 44 00 00 00      call *0x44
# Data will be placed starting here

使返回地址直接指向数据。那么问题来了:如何填充指令以使其末尾对齐?

编辑一点背景(对于那些还没有猜到的人)。我正在尝试实现闭包。在语言中,

(int -> int) make_curried_adder(int x)
{
    return int lambda (int y) { return x + y; };
}

(int -> int) plus7;
plus7 = make_curried_adder(7);
print("7 + 5 = ", plus7(5));

{ return x + y } 被翻译成一个普通但匿名的两个参数函数。分配一块内存并用存根指令、函数地址和值 7 填充。这由 make_curried_adder 返回,调用时会将附加参数 7 压入堆栈,然后跳转到匿名函数。

更新

我接受了 Pascal 的回答,即汇编程序往往被编写为一次性运行。我认为一些汇编程序确实有不止一次处理像“call x; ... ; x: ...”这样的代码,它有一个前向引用。 (事实上​​,我很久以前就写过一个——一旦到达 x,它就会返回并填写正确的地址。)或者也许所有这些漏洞都留给链接器关闭。 end-padding 的另一个问题是你需要语法说“插入填充 here 以便 there 对齐”。我可以想到一种算法可以适用于这样的简单情况,但它可能是一个晦涩难懂的功能,不值得实现。使用嵌套填充的更复杂的情况可能会产生矛盾的结果...

【问题讨论】:

  • 你想做什么?使用 jmp 而不是调用会起作用吗?
  • 跳跃效果肯定是我想要的;但是我需要的另一件事是数据的地址,它可以在内存中的任何位置,但总是靠近调用(或跳转)指令。 Call 方便地为我将 EIP 放入堆栈。
  • 出于好奇,这是用于什么语言的?这是将以源代码形式发布的东西吗?
  • 一个业余编译器,从“类 C”语言(即有很多 {},上面的示例)到 i386 机器代码。在使用会计软件 1 小时和在其上进行文书工作 7 小时后只需一些补偿。我的目标是省略一些东西(类型语法、指针)并添加一些其他东西(字符串、元组、闭包、gc)。它目前可以很好地编译基本整数算术(如果效率低,因为它不进行寄存器重新分配),并且可以进行 TCO 和内联。到目前为止,仅部分支持元组。此问题的代码构成 RT 库的一部分。

标签: c assembly


【解决方案1】:

不幸的是,大多数汇编器都是一次性的简单翻译器,这限制了它们可以提供的对齐指令的灵活性。即使在多次工作的组装人员可以提供的所有对齐选项中,也有许多被忽略了,因为它们太具体了。恐怕你的就是其中之一。只要它只是您打算移动的一条指令,它就可以在一次性汇编器中工作,但它非常具体。

我看过一个复杂的多遍汇编器的手册,它可以让你减去两个标签的地址以获得指令序列的长度,并让你插入一个指令来插入一个 NOP 序列,比如, (4 - 这个长度模 4)代替您选择的位置(只要它仍然可以收敛到每条指令的确定位置)。我不记得它是什么汇编程序。绝对不是gas,据我所知,这是一次性的。它可能是古老的A386。

【讨论】:

  • 我很怀疑——我猜汇编器不知道在指令#1 之前放置多少填充,直到它弄清楚指令#n 的放置位置。这是可以理解的,没有它我就无法管理。 (手动调整原来是“addl $3, %eax; andl $0xFFFFFFFC, %eax”...)
【解决方案2】:

call 之前添加自己的xchg 指令是否有问题?由于您在存根之前有一个对齐,因此对齐应该是一致的。

【讨论】:

  • 仅在 a) 我想让汇编器这样做,因为 b) 在我编写的汇编代码和生成的指令之间有一层翻译,而我没有不想按照“调用 * 总是恰好生成 6 个字节”的方式做出太多假设。
  • "call * 总是恰好生成 6 个字节" - 这种控制级别是您首先进行组装的原因。 IMO 我认为没有任何汇编指令可以对齐指令的结尾而不是开头。
【解决方案3】:

您是否考虑过将数据放在代码之前?

这样,它只是一个减法(存根代码的长度加上一些恒定的偏移量)来获得数据的地址,所以它是一条指令,而不是你准备接受的两条指令。而且我相信gas 会毫无问题地为您提供存根代码的长度(作为两个标签的差异),因为标签是在这种情况下定义后使用的。

假设数据由 32 位字组成,与您的初始解决方案相比,涉及的填充也更少(尽管我不确定为什么在您的初始解决方案中有这么多 .align 指令,可能是您的一些正交约束没有进入)。

【讨论】:

  • 这意味着指向这个(数据,代码)块的指针将不能直接调用,这很遗憾,因为能够将它作为回调传递给C代码是很好的。我可以返回 block+sizeof(data),但是我怀疑我的 GC(尚未写入)不会将该指针识别为依赖于分配的块。
  • 中缀指针一般很难管理,但是如果你要自己编写GC,可以考虑中缀指针的这种特殊情况。虽然您通常有 (header, data) 块,而 GC 使用 pointer[-1] 访问标头,但您将在此处 (data, header, code) 使用指向代码的指针、指向代码的指针以及指示距离的标头块到达两个方向。您可以将 C 代码传递给它,因为静态 C 代码不在堆中是可以识别的,因此在这种情况下 GC 不会查找标头。