用天然气进口的正确方法
Proper way to do imports with gas
根据我之前的两个问题——一个与导入常量相关,一个与导入函数相关——Import constants in x86 with gas and ,我想知道以下内容是否准确地总结了如何使用 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 , %edi
mov $SYS_WRITE, %eax
syscall
ret
# Local function (for now)
get_string_length:
mov [=11=], %eax # string length goes in rax
L1_get_string_length:
cmp [=11=], (%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 [=12=], %edi
mov $SYS_EXIT, %eax
syscall
如果我的理解正确的话:
- 需要使函数
.globl
在 linking 期间可供其他对象文件访问。这两个目标文件需要 link 编辑在一起,例如:ld file.o utils.o -o file
.
- 定义或宏可以 imported/included 使用
.include "filename"
。这实际上是 copy/paste 将该包含文件的内容放入该指令所在的位置。我们不需要 link —— 或者做任何额外的事情 —— 该文件的 .include
语句。如果多个文件使用相同的 include 语句有关系吗?
- 我可能遗漏的任何其他东西或有关导入、包含等的提示?
.include
是否采用标准的 unix 路径,例如我可以这样做:.include "../constants.s"
或 .include "/home/constants.s"
?
这里有四种可能的方法来“从文件中导入常量”。
1。使用 .include
和 =
(仅使用气体)
constants.inc:
ANSWER_TO_LIFE = 0x42
code.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 位 sign-extended 立即操作数(操作码 0x83)的较小的 3 字节编码。由于 0x42 适合 8 位,后者在这里可用,因此 add [=31=]x42, %ebx
可以用三个字节编码为 83 c3 42
。该示例还表明我们可以在汇编时对常量执行任意算术运算。
2。使用 C 预处理器(在实践中最常见)
constants.h:
#define ANSWER_TO_LIFE 0x42
code.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-style #include
和 #define
指令被扩展,所以汇编器只看到 mov [=38=]x42, %eax
而没有任何迹象表明常量曾经有过名称。
这种方法的优点是文件 constants.h
同样可以很好地包含到 C 代码中,这在项目混合 C 和汇编源代码的非常常见的情况下很有帮助。因此,这是我“在野外”最常看到的方法。 (几乎没有 real-life 程序是完全用汇编语言编写的。)
在您的原始用例中,所讨论的常量是一个 Linux 系统调用号,这种方法是最好的,因为相关的包含文件已经由内核开发人员编写,您可以获取它#include <asm/unistd.h>
。该文件定义了所有具有 __NR_exit
.
形式的宏名称的系统调用号
3。作为在 link 时间解析的符号(有点尴尬)
constants.s:
.global ANSWER_TO_LIFE
ANSWER_TO_LIFE = 0x42
code.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在评论中提到的方法。它把符号 ANSWER_TO_LIFE
当作恰好位于绝对地址 0x42
的标签。汇编程序将其视为任何其他标签;它在汇编时不知道它的地址,所以它将它作为未解析的引用留在目标文件 code.o
中,linker 最终会解析它。
我能看到的这种方法的唯一真正好处是,如果我们想改变常量的值,比如 0x43,我们不必 re-run 我们所有源代码上的汇编程序文件 code.s code2.s ...
;我们只需要 re-assemble constant.s
和 re-link。所以我们节省了一点构建时间,但不会太多,因为无论如何汇编代码通常都非常快。 (如果我们从 C 或 C++ 代码中引用符号可能会有所不同,编译速度较慢,但请参阅下文。)
但也有一些明显的缺点:
由于汇编器不知道常量的值,它必须假定它可能是对使用它的每条指令有效的最大大小。特别是,在 add $ANSWER_TO_LIFE, %ebx
中,它不能假定 8 位 0x83 编码可用,因此它必须 select 更大的 32 位编码。因此指令 add $ANSWER_TO_LIFE, %ebx
必须被汇编为 81 c3 00 00 00 00
,其中 00 00 00 00
被具有正确值 42 00 00 00
的 link 替换。但是我们最终在一条指令上使用了 6 个字节,而理想情况下本可以使用 3 个字节进行编码。
另一方面,立即数 mov
到 64 位寄存器也有两种编码:一种采用 sign-extended 32 位立即数 mov $imm32, %reg64
(带有 REX.W 前缀的操作码 c7),这是 7 个字节,另一个采用完整的 64 位立即数 mov $imm64, %reg64
(带有 REX.W 的操作码 b8-b4),这是 10 个字节。汇编程序默认 select 32 位形式,因为 64 位形式真的很长而且很少需要。但是如果事实证明你的符号有一个不适合 32 位的值,你会在 link 时间得到一个错误(“重定位 t运行cated to fit”),你必须返回并使用助记符 movabs
强制使用 64 位编码。如果您使用了方法 1 或 2,汇编器就会知道您的常量的值,并且会首先 selected 适当的编码。
如果我们想对常量进行 build-time 算术运算,我们只能使用任何算术运算一个在目标文件中被表示为重定位。常量偏移有效,所以 mov $(ANSWER_TO_LIFE+17), %ecx
没问题;目标文件告诉 linker 用符号 ANSWER_TO_LIFE
的值加上常量 17 填充相关字节。 a static struct
.) 但是不支持像乘法这样的更通用的操作,因为人们通常不想在地址上做这些操作,所以 mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %edx
会导致汇编程序出错。如果我们需要生命答案的平方,我们必须写一个 mul
指令在 运行 时间计算它,如果这是经常调用并且需要被调用的代码,那将没有乐趣快。
常量也可以从 C 代码访问 linked 到我们的项目中,但它必须像标签(变量的地址)一样对待,这使它看起来很奇怪。我们必须写类似
的东西
extern void *ANSWER_TO_LIFE;
printf("The answer is %lu\n", (unsigned long)&ANSWER_TO_LIFE);
如果我们尝试写更多natural-looking像
extern unsigned long ANSWER_TO_LIFE;
printf("The answer is %lu\n", ANSWER_TO_LIFE);
程序将尝试从内存地址 0x42 获取值,这将导致崩溃。
(此外,即使在第一个示例中,编译器的汇编输出也使用 mov
助记符,这再次导致汇编程序 select 进行 32 位移动。如果 ANSWER_TO_LIFE
大于 2^32
那么 linking 就会失败,这次修复起来就没那么容易了。据我所知,你需要给 gcc 一个适当的选项来告诉它改变它的 code model,这将导致 每个 地址加载使用效率较低的 64 位形式,并且您必须对整个程序执行此操作。)
4。作为存储在内存中并在 运行 时间获取的值(低效)
constants.s:
.section .rodata
.global answer_to_life
answer_to_life:
.int 0x42
code.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 , %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 存储在我们程序的内存中,每当我们需要访问它时,我们都需要一条从内存中读取的指令;我们不能再将其编码为每条指令中的立即数。这通常执行起来较慢。如果我们需要对其进行任何运算,我们必须编写代码将其加载到寄存器中并在 运行 时间执行适当的指令,这需要周期和代码 space.
我已将此处的名称更改为小写,以匹配位于内存中的变量的约定,而不是不再是“编译时”常量。还要注意说明中的不同语法; mov answer_to_life, %eax
没有 $
符号,是从内存加载而不是立即移动。本例中的 $answer_to_life
为您提供了变量的地址(巧合的是,在我的测试程序中是 0x402000
)。如果你想构建一个 position-independent 可执行文件,这是现代 Linux 程序的标准,你需要编写 answer_to_life(%rip)
来代替。
由于上述原因,这种方法对于在编译时真正已知的数字常量并不理想,但为了完整性我将其包括在内,因为您在评论中询问了它。
根据我之前的两个问题——一个与导入常量相关,一个与导入函数相关——Import constants in x86 with gas and 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 , %edi
mov $SYS_WRITE, %eax
syscall
ret
# Local function (for now)
get_string_length:
mov [=11=], %eax # string length goes in rax
L1_get_string_length:
cmp [=11=], (%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 [=12=], %edi
mov $SYS_EXIT, %eax
syscall
如果我的理解正确的话:
- 需要使函数
.globl
在 linking 期间可供其他对象文件访问。这两个目标文件需要 link 编辑在一起,例如:ld file.o utils.o -o file
. - 定义或宏可以 imported/included 使用
.include "filename"
。这实际上是 copy/paste 将该包含文件的内容放入该指令所在的位置。我们不需要 link —— 或者做任何额外的事情 —— 该文件的.include
语句。如果多个文件使用相同的 include 语句有关系吗? - 我可能遗漏的任何其他东西或有关导入、包含等的提示?
.include
是否采用标准的 unix 路径,例如我可以这样做:.include "../constants.s"
或.include "/home/constants.s"
?
这里有四种可能的方法来“从文件中导入常量”。
1。使用 .include
和 =
(仅使用气体)
constants.inc:
ANSWER_TO_LIFE = 0x42
code.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 位 sign-extended 立即操作数(操作码 0x83)的较小的 3 字节编码。由于 0x42 适合 8 位,后者在这里可用,因此 add [=31=]x42, %ebx
可以用三个字节编码为 83 c3 42
。该示例还表明我们可以在汇编时对常量执行任意算术运算。
2。使用 C 预处理器(在实践中最常见)
constants.h:
#define ANSWER_TO_LIFE 0x42
code.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-style #include
和 #define
指令被扩展,所以汇编器只看到 mov [=38=]x42, %eax
而没有任何迹象表明常量曾经有过名称。
这种方法的优点是文件 constants.h
同样可以很好地包含到 C 代码中,这在项目混合 C 和汇编源代码的非常常见的情况下很有帮助。因此,这是我“在野外”最常看到的方法。 (几乎没有 real-life 程序是完全用汇编语言编写的。)
在您的原始用例中,所讨论的常量是一个 Linux 系统调用号,这种方法是最好的,因为相关的包含文件已经由内核开发人员编写,您可以获取它#include <asm/unistd.h>
。该文件定义了所有具有 __NR_exit
.
3。作为在 link 时间解析的符号(有点尴尬)
constants.s:
.global ANSWER_TO_LIFE
ANSWER_TO_LIFE = 0x42
code.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在评论中提到的方法。它把符号 ANSWER_TO_LIFE
当作恰好位于绝对地址 0x42
的标签。汇编程序将其视为任何其他标签;它在汇编时不知道它的地址,所以它将它作为未解析的引用留在目标文件 code.o
中,linker 最终会解析它。
我能看到的这种方法的唯一真正好处是,如果我们想改变常量的值,比如 0x43,我们不必 re-run 我们所有源代码上的汇编程序文件 code.s code2.s ...
;我们只需要 re-assemble constant.s
和 re-link。所以我们节省了一点构建时间,但不会太多,因为无论如何汇编代码通常都非常快。 (如果我们从 C 或 C++ 代码中引用符号可能会有所不同,编译速度较慢,但请参阅下文。)
但也有一些明显的缺点:
由于汇编器不知道常量的值,它必须假定它可能是对使用它的每条指令有效的最大大小。特别是,在
add $ANSWER_TO_LIFE, %ebx
中,它不能假定 8 位 0x83 编码可用,因此它必须 select 更大的 32 位编码。因此指令add $ANSWER_TO_LIFE, %ebx
必须被汇编为81 c3 00 00 00 00
,其中00 00 00 00
被具有正确值42 00 00 00
的 link 替换。但是我们最终在一条指令上使用了 6 个字节,而理想情况下本可以使用 3 个字节进行编码。另一方面,立即数
mov
到 64 位寄存器也有两种编码:一种采用 sign-extended 32 位立即数mov $imm32, %reg64
(带有 REX.W 前缀的操作码 c7),这是 7 个字节,另一个采用完整的 64 位立即数mov $imm64, %reg64
(带有 REX.W 的操作码 b8-b4),这是 10 个字节。汇编程序默认 select 32 位形式,因为 64 位形式真的很长而且很少需要。但是如果事实证明你的符号有一个不适合 32 位的值,你会在 link 时间得到一个错误(“重定位 t运行cated to fit”),你必须返回并使用助记符movabs
强制使用 64 位编码。如果您使用了方法 1 或 2,汇编器就会知道您的常量的值,并且会首先 selected 适当的编码。如果我们想对常量进行 build-time 算术运算,我们只能使用任何算术运算一个在目标文件中被表示为重定位。常量偏移有效,所以
mov $(ANSWER_TO_LIFE+17), %ecx
没问题;目标文件告诉 linker 用符号ANSWER_TO_LIFE
的值加上常量 17 填充相关字节。 a staticstruct
.) 但是不支持像乘法这样的更通用的操作,因为人们通常不想在地址上做这些操作,所以mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE), %edx
会导致汇编程序出错。如果我们需要生命答案的平方,我们必须写一个mul
指令在 运行 时间计算它,如果这是经常调用并且需要被调用的代码,那将没有乐趣快。
常量也可以从 C 代码访问 linked 到我们的项目中,但它必须像标签(变量的地址)一样对待,这使它看起来很奇怪。我们必须写类似
的东西extern void *ANSWER_TO_LIFE;
printf("The answer is %lu\n", (unsigned long)&ANSWER_TO_LIFE);
如果我们尝试写更多natural-looking像
extern unsigned long ANSWER_TO_LIFE;
printf("The answer is %lu\n", ANSWER_TO_LIFE);
程序将尝试从内存地址 0x42 获取值,这将导致崩溃。
(此外,即使在第一个示例中,编译器的汇编输出也使用 mov
助记符,这再次导致汇编程序 select 进行 32 位移动。如果 ANSWER_TO_LIFE
大于 2^32
那么 linking 就会失败,这次修复起来就没那么容易了。据我所知,你需要给 gcc 一个适当的选项来告诉它改变它的 code model,这将导致 每个 地址加载使用效率较低的 64 位形式,并且您必须对整个程序执行此操作。)
4。作为存储在内存中并在 运行 时间获取的值(低效)
constants.s:
.section .rodata
.global answer_to_life
answer_to_life:
.int 0x42
code.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 , %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 存储在我们程序的内存中,每当我们需要访问它时,我们都需要一条从内存中读取的指令;我们不能再将其编码为每条指令中的立即数。这通常执行起来较慢。如果我们需要对其进行任何运算,我们必须编写代码将其加载到寄存器中并在 运行 时间执行适当的指令,这需要周期和代码 space.
我已将此处的名称更改为小写,以匹配位于内存中的变量的约定,而不是不再是“编译时”常量。还要注意说明中的不同语法; mov answer_to_life, %eax
没有 $
符号,是从内存加载而不是立即移动。本例中的 $answer_to_life
为您提供了变量的地址(巧合的是,在我的测试程序中是 0x402000
)。如果你想构建一个 position-independent 可执行文件,这是现代 Linux 程序的标准,你需要编写 answer_to_life(%rip)
来代替。
由于上述原因,这种方法对于在编译时真正已知的数字常量并不理想,但为了完整性我将其包括在内,因为您在评论中询问了它。