【问题标题】:Proper way to do imports with gas使用气体进行进口的正确方法
【发布时间】:2020-09-22 20:34:55
【问题描述】:

根据我之前的两个问题——一个与导入常量有关,一个与导入函数有关——Import constants in x86 with gasWhy does this program loop?,我想知道以下是否准确总结了如何在汇编中使用 as 进行导入一个例子:

# constants.s
SYS_EXIT        = 60
SYS_WRITE       = 1
STDOUT_FILENO   = 1
# utils.s
.include "constants.s"

# Global function
.globl print_string
print_string:

    call get_string_length
    mov %eax, %edx
    mov %rdi,       %rsi
    mov $1,         %edi
    mov $SYS_WRITE, %eax
    syscall
    ret

# Local function (for now)
get_string_length:
    mov $0, %eax # string length goes in rax
  L1_get_string_length:
    cmp $0, (%rdi, %rax,)
    je L2_get_string_length
    inc %eax
    jmp L1_get_string_length
  L2_get_string_length:
    ret
# file.s
.include "constants.s"

.data
str:    .string "Hellllloooo"

.text
.globl _start
_start:
    mov $str,   %rdi
    call print_string
    mov $0, %edi
    mov $SYS_EXIT, %eax
    syscall

如果我的理解是正确的,那么:

  1. 需要在链接过程中设置函数.globl 才能被其他目标文件访问。这两个目标文件都需要链接在一起,例如:ld file.o utils.o -o file
  2. 可以使用.include "filename" 导入/包含定义或宏。这实际上是将包含文件的内容复制/粘贴到该指令所在的位置。我们不需要链接——或做任何其他事情——该文件的.include 语句。多个文件是否使用相同的包含语句有关系吗?
  3. 我可能遗漏的任何其他内容或关于导入、包含等的提示? .include 是否采用标准的 unix 路径,例如我可以这样做:.include "../constants.s".include "/home/constants.s"

【问题讨论】:

  • 请注意,虽然您可以使用.include 使常量可见,但您也可以像处理函数一样将常量声明为全局常量,并让链接器解决这个问题。但是,请注意,这意味着必须在程序中的任何地方都使用具有相同值的常量。即使在 3rd 方库中。
  • @fuz 感谢您的反馈。我将如何制作一个恒定的全局变量?比如:.globl SYS_EXIT; SYS_EXIT: .int 60?
  • @samuelbrody1249:那是另一回事,因为在这种情况下,$SYS_EXIT 不会给你数字 60,而是在内存中存储数字 60 的地址。您必须立即将 mov $SYS_EXIT, %eax 更改为 mov SYS_EXIT, %eax,尽管这在 A&T& 符号中并不明显,但它是内存负载,效率较低。
  • 链接器符号旨在作为地址,而不是数字常量,因此如何将 1 设置为常量并不那么明显。它可能可以完成,但通常不会。一个问题是,使其成为链接器符号通常会导致无法对其进行任何重要的编译时算术。您仅限于任何可用的重定位转换。
  • @samuelbrody1249 只需将.globl SYS_EXIT 添加到您现有的SYS_EXIT = 60

标签: assembly x86 linker gnu-assembler


【解决方案1】:

以下是“从文件中导入常量”的四种可能方式。

1。使用.include=(仅使用gas)

constants.inc:

    ANSWER_TO_LIFE = 0x42

代码.s:

    .include "constants.inc"
    mov $ANSWER_TO_LIFE, %eax
    add $ANSWER_TO_LIFE, %ebx   # best encoding
    mov $(ANSWER_TO_LIFE+17), %ecx
    mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %edx

建筑:

as -o code.o code.s         # or gcc -c code.s
ld -o prog code.o code2.o   # or gcc -o prog code.o code2.o

这是最直接的方法,只使用 GNU 汇编器本身的特性。我将包含文件命名为.inc 而不是.s,以表明它应该包含在其他程序集源文件中,但不会自行组装(因为它会生成一个不包含任何内容的目标文件)。您可以根据需要将其包含到尽可能多的不同文件中以使用该常量,并且支持相对或绝对路径(.include ../include/constants.inc.include /usr/share/include/constants.inc 都可以)。

由于汇编器知道常量的值,它可以选择最佳的指令编码。例如,the x86 add $imm, %reg32 instruction has two possible encodings:带有 32 位立即操作数(操作码 0x81)的 6 字节编码,以及带有 8 位符号扩展立即操作数(操作码 0x83)的较小 3 字节编码。由于 0x42 适合 8 位,因此后者在这里可用,因此add $0x42, %ebx 可以用三个字节编码为83 c3 42。该示例还表明,我们可以在汇编时对常量执行任意算术运算。

2。使用 C 预处理器(在实践中最常见)

常量.h:

#define ANSWER_TO_LIFE 0x42

代码.S:

#include "constants.h"
    mov $ANSWER_TO_LIFE, %eax
    add $ANSWER_TO_LIFE, %ebx   # also gets best encoding
    mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %ecx

建筑:

gcc -c code.S              # can't use as by itself here
ld -o prog code.o code2.o  # or gcc if you prefer

在这种方法中,您在将源文件提供给汇编程序之前对源文件运行 C 预处理器 cpp。如果您使用.S 命名源文件,gcc 命令将为您执行此操作(注意区分大小写)。然后 C 风格的 #include#define 指令被扩展,因此汇编器只看到 mov $0x42, %eax 而没有任何迹象表明该常量曾经有过名称。

这种方法的优点是constants.h 文件同样可以包含在 C 代码中,这在您的项目混合 C 和汇编源代码的非常常见的情况下很有帮助。因此,这是我“在野外”最常看到的方法。 (实际上,现实生活中没有完全用汇编语言编写的程序。)

在您最初的用例中,所讨论的常量是 Linux 系统调用号,这种方法是最好的,因为相关的包含文件已经由内核开发人员编写,您可以通过 #include <asm/unistd.h> 获得它。该文件定义了所有系统调用号,其宏名称格式为__NR_exit

3。作为链接时解析的符号(有点尴尬)

常量.s:

    .global ANSWER_TO_LIFE
    ANSWER_TO_LIFE = 0x42

代码.s:

    mov $ANSWER_TO_LIFE, %eax
    add $ANSWER_TO_LIFE, %ebx   # not the optimal encoding
    mov $(ANSWER_TO_LIFE+17), %ecx
    #mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %ecx # error

建筑:

as -o constants.o constants.s          # or gcc -c constants.s
as -o code.o code.s                    # etc
ld -o prog constants.o code.o code2.o  # or gcc

这是 @fuz 在 cmets 中提到的方法。它将符号ANSWER_TO_LIFE 视为恰好位于绝对地址0x42 的标签。汇编器将其视为任何其他标签;它在汇编时不知道它的地址,因此它将它作为未解析的引用留在目标文件code.o 中,链接器最终会解析它。

我可以看到这种方法的唯一真正好处是,如果我们想更改常量的值,比如 0x43,我们不必对所有源文件重新运行汇编程序code.s code2.s ... ;我们只需要重新组装constant.s 并重新链接。所以我们节省了一点构建时间,但并不多,因为汇编代码通常非常快。 (如果我们从 C 或 C++ 代码中引用符号可能会有所不同,编译速度较慢,但​​请参见下文。)

但也有一些明显的缺点:

  • 由于汇编器不知道常量的值,它必须假设它可能是对使用它的每条指令有效的最大大小。特别是在add $ANSWER_TO_LIFE, %ebx中,不能假设8位0x83编码就可以使用,所以必须选择更大的32位编码。因此指令add $ANSWER_TO_LIFE, %ebx 必须汇编为81 c3 00 00 00 00,其中00 00 00 00 被链接器替换为具有正确值42 00 00 00。但是我们最终在一条指令上使用了 6 个字节,而理想情况下本可以使用 3 个字节进行编码。

  • 另一方面,立即数 mov 进入 64 位寄存器也有两种编码:一种采用符号扩展的 32 位立即数 mov $imm32, %reg64(操作码 c7 带有 REX.W 前缀),即7 个字节,另一个采用完整的 64 位立即数 mov $imm64, %reg64(操作码 b8-b4 和 REX.W),即 10 个字节。汇编器默认选择 32 位格式,因为 64 位格式真的很长而且很少需要。但如果事实证明您的符号的值不适合 32 位,您将在链接时收到错误(“重定位被截断以适合”),您将不得不返回并强制 64使用助记符movabs 进行位编码。如果您使用了方法 1 或 2,那么汇编器会知道您的常量的值,并且会首先选择适当的编码。

  • 如果我们想对常量进行构建时算术,我们将受限于可以在目标文件中表示为重定位的任何算术。恒定偏移有效,所以mov $(ANSWER_TO_LIFE+17), %ecx 可以;目标文件告诉链接器用符号 ANSWER_TO_LIFE 加上常量 17 的值填充相关字节。(对于实际的标签,你会想要这个,比如从静态 struct 访问成员。)但是不支持像乘法这样的更一般的操作,因为人们通常不想在地址上执行这些操作,所以mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %edx 会导致汇编器出错。如果我们需要生活答案的平方,我们必须编写一个mul 指令来在运行时计算它,如果这是频繁调用且需要快速的代码,这将没有乐趣。

也可以从链接到我们项目的 C 代码中访问该常量,但必须将其视为标签(变量的地址),这使它看起来很奇怪。我们必须写一些类似的东西

extern void *ANSWER_TO_LIFE;
printf("The answer is %lu\n", (unsigned long)&ANSWER_TO_LIFE);

如果我们尝试写一些更自然的东西

extern unsigned long ANSWER_TO_LIFE;
printf("The answer is %lu\n", ANSWER_TO_LIFE);

程序将尝试从内存地址 0x42 获取值,这将崩溃。

(此外,即使在第一个示例中,编译器输出的汇编程序使用 mov 助记符,这再次导致汇编程序选择 32 位移动。如果 ANSWER_TO_LIFE 大于 2^32,则链接将失败,这一次修复起来并不容易。AFAIK 你需要给 gcc 一个适当的选项来告诉它更改其code model,这将导致 每个 地址加载使用 less高效的 64 位形式,您必须为整个程序执行此操作。)

4。作为存储在内存中并在运行时获取的值(低效)

常量.s:

    .section .rodata
    .global answer_to_life
answer_to_life:
    .int 0x42

代码.s:

    mov answer_to_life, %eax
    add answer_to_life, %ebx

    # mov answer_to_life+17, %ecx # not valid, no such instruction exists
    mov answer_to_life, %ecx
    add $17, %ecx   # needs two instructions

    # mov answer_to_life*answer_to_life, %edx # not valid
    mov answer_to_life, %eax
    mul %eax  # clobbers %edx

建筑:

as -o constants.o constants.s
as -o code.o code.s
ld -o prog constants.o code.o code2.o

这种方法相当于在 C 程序中使用 const int answer_to_life = 42;(尽管 C++ 不同)。值 42 存储在我们程序的内存中,当我们需要访问它时,我们需要一条从内存中读取的指令;我们不能再在每条指令中将其编码为立即数。这通常会执行较慢。如果我们需要对其进行任何算术运算,我们必须编写代码将其加载到寄存器中并在运行时执行适当的指令,这需要周期和代码空间。

我已将此处的名称更改为小写以匹配位于内存中的变量的约定,而不是不再是“编译时”常量。还要注意说明中的不同语法; mov answer_to_life, %eax,没有$ 符号,是从内存加载而不是立即移动。在这个例子中$answer_to_life 给了你变量的地址(巧合的是,在我的测试程序中是0x402000)。如果您希望能够构建与位置无关的可执行文件,这是现代 Linux 程序的规范,您需要改为编写 answer_to_life(%rip)

由于上述原因,这种方法对于在编译时真正已知的数字常量并不理想,但我将其包括在内是为了完整性,并且因为您在 cmets 中询问过它。

【讨论】:

  • 非常感谢您抽出宝贵时间。多么棒且非常有帮助的答案!
猜你喜欢
  • 1970-01-01
  • 2012-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-19
  • 2021-11-12
  • 2014-07-02
  • 1970-01-01
相关资源
最近更新 更多