[英]Proper way to do imports with gas
从我之前的两个问题——一个与导入常量相关,一个与导入函数相关—— 用气体导入 x86 中的常量和为什么这个程序循环? ,我想知道以下是否准确地总结了如何使用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
如果我的理解是正确的,那么:
.globl
以便在链接期间其他 object 文件可以访问。 这两个 object 文件都需要链接在一起,例如: ld file.o utils.o -o file
。.include "filename"
导入/包含定义或宏。 这实际上是将包含文件的内容复制/粘贴到该指令所在的位置。 我们不需要链接——或做任何额外的事情——该文件的.include
语句。 多个文件使用相同的 include 语句有关系吗?.include
是否采用标准的 unix 路径,例如我可以这样做: .include "../constants.s"
或.include "/home/constants.s"
?这里有四种可能的方法来“从文件中导入常量”。
.include
和=
(仅使用 gas)constants.inc:
ANSWER_TO_LIFE = 0x42
代码:
.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
,以表明它是要包含到其他汇编源文件中,而不是单独汇编(因为它会生成一个什么都不包含的 object 文件)。 您可以将它包含到尽可能多的不同文件中,因为需要使用常量,并且支持相对或绝对路径( .include../include/constants.inc
, .include /usr/share/include/constants.inc
都可以) .
由于汇编程序知道常量的值,因此它可以选择最佳的指令编码。 例如, x86 add $imm, %reg32
指令有两种可能的编码:带有 32 位立即操作数的 6 字节编码(操作码 0x81),以及带有 8 位符号扩展立即数的较小的 3 字节编码操作数(操作码 0x83)。 由于 0x42 适合 8 位,后者在这里可用,所以add $0x42, %ebx
可以用三个字节编码为83 c3 42
。 该示例还表明我们可以在汇编时对常量执行任意算术运算。
常量.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 系统调用号,这种方法是最好的,因为相关的包含文件已经由 kernel 开发人员编写,您可以使用#include <asm/unistd.h>
。 该文件使用__NR_exit
形式的宏名称定义所有系统调用号。
常量.s:
.global ANSWER_TO_LIFE
ANSWER_TO_LIFE = 0x42
代码:
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 在评论中提到的方法。 它将符号ANSWER_TO_LIFE
视为恰好位于绝对地址0x42
的 label。 汇编程序将其视为任何其他 label; 它在汇编时不知道它的地址,所以它将它作为 object 文件code.o
中的未解析引用保留,linker 最终将解析该引用。
我能看到的这种方法的唯一真正好处是,如果我们想更改常量的值,比如 0x43,我们不必在所有源文件code.s code2.s...
上重新运行汇编程序code.s code2.s...
; 我们只需要重新组装constant.s
并重新链接。 所以我们节省了一点构建时间,但不会太多,因为无论如何汇编代码通常都非常快。 (如果我们引用 C 或 C++ 代码中的符号可能会有所不同,编译速度较慢,但请参见下文。)
但也有一些明显的缺点:
由于汇编程序不知道常量的值,因此它必须假设它可能是对使用它的每条指令有效的最大大小。 特别是,在add $ANSWER_TO_LIFE, %ebx
中,它不能假设 8 位 0x83 编码可用,因此它必须使用更大的 32 位编码 select。 因此指令add $ANSWER_TO_LIFE, %ebx
必须汇编为81 c3 00 00 00 00
,其中00 00 00 00
被 linker 替换为正确的值42 00 00 00
。 但是我们最终在一条指令上使用了 6 个字节,而理想情况下本可以使用 3 个字节进行编码。
另一方面,直接移动到 64 位寄存器也有两种编码:一种采用符号扩展的 32 位立即数mov
mov $imm32, %reg64
(带有 REX.W 前缀的操作码 c7),这是 7 个字节,另一个采用完整的 64 位立即mov $imm64, %reg64
(操作码 b8-b4 和 REX.W),这是 10 个字节。 汇编器默认 select 是 32 位的形式,因为 64 位的真的很长而且很少需要。 但是如果事实证明你的符号有一个不适合 32 位的值,你会在链接时得到一个错误(“重定位被截断以适合”),你必须返回 go 并强制 64位编码使用助记符movabs
。 如果您使用了方法 1 或 2,汇编器就会知道您的常量的值,并且会首先选择适当的编码。
如果我们想对常量进行构建时算术运算,我们只能使用 object 文件中可以表示为重定位的任何算术运算。 常量偏移有效,所以mov $(ANSWER_TO_LIFE+17), %ecx
没问题; object 文件告诉 linker 用符号ANSWER_TO_LIFE
的值加上常量 17 填充相关字节。(对于实际标签,您希望这样做,例如从 static struct
访问成员。)但是更一般的操作不支持类似乘法,因为人们通常不想在地址上执行这些操作,因此mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %edx
会导致汇编程序出错。 如果我们需要生命答案的平方,我们必须编写一个mul
指令来在运行时计算它,如果这是频繁调用且需要快速的代码,那将没有乐趣。
常量也可以从链接到我们项目的 C 代码访问,但它必须像 label(变量地址)一样对待,这使得它看起来很奇怪。 我们必须写类似的东西
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);
该程序将尝试从 memory 地址 0x42 获取值,这将崩溃。
(此外,即使在第一个示例中,编译器的程序集 output 也使用mov
助记符,这再次导致汇编器选择 32 位移动。如果ANSWER_TO_LIFE
大于2^32
,则链接将失败,这次不会as easy to fix. AFAIK 你需要给 gcc 一个适当的选项来告诉它改变它的代码 model ,这会导致每个地址加载使用效率较低的 64 位形式,你必须这样做你的整个程序。)
常量.s:
.section .rodata
.global answer_to_life
answer_to_life:
.int 0x42
代码:
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
这种方法相当于const int answer_to_life = 42;
在 C 程序中(尽管 C++ 不同)。 值 42 存储在我们程序的 memory 中,每当我们需要访问它时,我们都需要一条从 memory 读取的指令; 我们不能再将其编码为每条指令中的立即数。 这通常执行起来较慢。 如果我们需要对其进行任何运算,我们必须编写代码将其加载到寄存器中并在运行时执行适当的指令,这需要周期和代码空间。
我已将此处的名称更改为小写,以匹配位于 memory 中的变量的约定,而不是不再是“编译时”常量。 还要注意说明中的不同语法; mov answer_to_life, %eax
没有$
符号,是从 memory 加载而不是立即移动。 本例中的$answer_to_life
为您提供了变量的地址(巧合的是,在我的测试程序中它是0x402000
)。 如果您希望能够构建与位置无关的可执行文件(这是现代 Linux 程序的规范),您需要改为编写answer_to_life(%rip)
。
由于上述原因,这种方法对于在编译时真正已知的数值常量并不理想,但为了完整性,我将其包括在内,因为您在评论中询问了它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.