如何解决用天然气进口的正确方法
从我之前的两个问题(一个与导入常量有关,一个与导入函数有关)-Why does this program loop?和.include "filename"
,我想知道以下内容是否准确总结了如何使用{{ 1}},并附带示例:
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
。 - 可以使用https://www.w3schools.com/js/js_loop_for.asp导入/包含定义或宏。这实际上是将包含的文件的内容复制/粘贴到该指令所在的位置。除了该文件的
ld file.o utils.o -o file
语句之外,我们不需要链接或做任何其他事情。多个文件是否使用相同的include语句有关系吗? - 我可能还缺少其他任何东西或有关导入,包含等的提示吗?
.include
是否采用标准的Unix路径,例如我可以这样做:.include
或.include "../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位符号扩展立即操作数(操作码0x83)的较小的3字节编码。由于0x42可以容纳8位,因此后者在此处可用,因此add $0x42,%ebx
可以在三个字节中编码为83 c3 42
。该示例还表明,我们可以在汇编时对常量执行任意运算。
2。使用C预处理器(实际上是最常用的)
constants.h:
#define ANSWER_TO_LIFE 0x42
code.S:
#include "constants.h"
mov $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
之前,将其交给了汇编程序。如果使用gcc
命名源文件,则.S
命令将为您执行此操作(注意区分大小写)。然后,扩展C样式的#include
和#define
伪指令,因此汇编器仅看到mov $0x42,%eax
,而没有任何迹象表明该常量曾经有一个名称。
这种方法的优点是文件constants.h
可以很好地包含在C代码中,这在项目将C和汇编源代码混合在一起的非常常见的情况下很有用。因此,这是我在“野外”最常看到的方法。 (实际上,没有现实生活中的程序是完全用汇编语言编写的。)
在您的原始用例中,所讨论的常量是Linux系统调用号,这种方法是最好的,因为相关的包含文件已经由内核开发人员编写,您可以使用#include <asm/unistd.h>
来获取它。 。该文件使用__NR_exit
形式的宏名称定义了所有系统调用号码。
3。作为符号在链接时解析(有点尴尬)
constants.s:
.global ANSWER_TO_LIFE
ANSWER_TO_LIFE = 0x42
code.s:
mov $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
中的未解析引用,链接器将最终对其进行解析。
这种方法的唯一真正好处是,如果我们要更改常量的值(例如0x43),则不必在所有源文件上重新运行汇编器{{1 }};我们只需要重新组装code.s code2.s ...
并重新链接。因此,我们节省了一些构建时间,但是并没有太多,因为反正汇编代码通常都非常快。 (如果我们从C或C ++代码中引用符号,可能会有所不同,因为C或C ++代码的编译速度较慢,但请参见下文。)
但是有一些明显的缺点:
-
由于汇编器不知道该常量的值,因此必须假定该常量的大小对于使用该常量的每个指令均有效。特别是在
constant.s
中,不能假定8位0x83编码将可用,因此必须选择较大的32位编码。因此,指令add $ANSWER_TO_LIFE,%ebx
必须被汇编为add $ANSWER_TO_LIFE,%ebx
,其中81 c3 00 00 00 00
被链接器替换为正确的值00 00 00 00
。但是我们最终在一条指令上使用了6个字节,理想情况下,本来可以使用3个字节进行编码。 -
与此相反,立即
mov
到64位寄存器中也有两种编码:一种采用带符号扩展的32位立即42 00 00 00
(带有REX的操作码c7) .W前缀),它是7个字节,另一个则是完整的64位立即数mov $imm32,%reg64
(带有REX.W的操作码b8-b4),它是10个字节。默认情况下,汇编器将选择32位格式,因为64位格式确实很长,几乎不需要。但是,如果事实证明您的符号的值不适合32位,则在链接时会收到错误消息(“重定位被截断以适合”),您必须返回并强制输入64使用助记符mov $imm64,%reg64
进行位编码。如果您使用的是方法1或2,则汇编程序将知道常数的值,并且首先将选择适当的编码。 -
如果要对常量执行构建时算术,则仅限于可以在目标文件中表示为重定位的任何算术。常数偏移量起作用,所以
movabs
可以;目标文件告诉链接器用符号mov $(ANSWER_TO_LIFE+17),%ecx
的值加常量17填充相关的字节。(对于实际标签,您希望这样做是为了从静态{{1 }}。)但不支持更通用的运算,例如乘法,因为人们通常不希望在地址上执行这些运算,因此ANSWER_TO_LIFE
导致汇编程序出错。如果我们需要求生答案的平方,就必须编写一条struct
指令来在运行时对其进行计算,如果这是经常调用且需要快速执行的代码,那将毫无乐趣。>
也可以从链接到我们项目中的C代码访问该常量,但必须将其视为标签(变量的地址),这使其看起来很奇怪。我们必须写类似
mov $(ANSWER_TO_LIFE*ANSWER_TO_LIFE),%edx
如果我们尝试写一些看起来更自然的东西
mul
程序将尝试从内存地址0x42中获取值,这将导致崩溃。
(此外,即使在第一个示例中,编译器的汇编输出也使用extern void *ANSWER_TO_LIFE;
printf("The answer is %lu\n",(unsigned long)&ANSWER_TO_LIFE);
助记符,这再次导致汇编器选择32位移动。如果extern unsigned long ANSWER_TO_LIFE;
printf("The answer is %lu\n",ANSWER_TO_LIFE);
大于{{ 1}},然后链接将失败,这一次不那么容易解决。AFAIK您需要给gcc一个适当的选项,告诉它更改其code model,这会导致每地址加载以使用效率较低的64位格式,并且您必须在整个程序中都这样做。)
4。作为存储在内存中并在运行时获取的值(效率低)
constants.s:
mov
code.s:
ANSWER_TO_LIFE
建筑物:
2^32
此方法等效于在C程序中使用 .section .rodata
.global answer_to_life
answer_to_life:
.int 0x42
(尽管C ++不同)。值42存储在程序的内存中,每当需要访问它时,就需要一条从内存中读取的指令。我们不能再将其编码为每个指令中的立即数。这通常执行起来较慢。如果需要对其进行任何算术运算,则必须编写代码以将其加载到寄存器中,并在运行时执行适当的指令,这会占用周期和代码空间。
我将此处的名称更改为小写,以匹配内存中变量的约定,而不是不再使用的“编译时”常量。还要注意说明中的不同语法。没有符号 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
的{{1}}是内存中的负载,而不是立即移动。在此示例中,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;
)。如果您希望能够构建位置无关的可执行文件(这是现代Linux程序的标准),则需要编写mov answer_to_life,%eax
。
由于上述原因,这种方法对于在编译时真正已知的数值常量并不理想,但出于完整性考虑,我将其包括在内,因为您在注释中提出了要求。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。