汇编程序伪操作指令
汇编程序的 . 伪操作
汇编程序中以.
开头的名称并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊指示,称为汇编指示(Assembler Directive)或伪操作(Pseudo-operation)
.section
指示把代码划分成若干个段(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的页面设置不同的读、写、执行权限。
.section .text
.text
段保存代码,是只读和可执行的,后面那些指令都属于.text
段。
.section .data
.data
段保存已经初始化的全局静态变量和局部静态变量,C程序普通局部变量在运行时被保存在堆栈中,既不出现在.data段中,也不出现在.bss段中。此外,如果变量被初始化值为0,也可能会放到bss段。
.section .rodata
rodata
段存放的是只读数据,一般是程序的只读变量和字符串常量。如C语言中的const
修饰的变量。单独设置rodata
段有很多好处,,不光是在语义上支持C/C++的const
关键字,而是操作系统在加载的时候可以将rodata
段的属性映射为只读,对于这个短的任何操作都会作为非法操作处理,提高了程序的安全性。另外,在嵌入式平台下一般也会存在只读存储器,如ROM,这样rodata
段放在该存储区域中就可以保证程序访问存储器的正确性。
.section .bss
bss
段保存未初始化的全局变量和局部静态变量。
.section .rodata1
rodata1
段存放只读数据,比如字符串常量、全局const变量,跟rodata
一样。
.section .comment
comment
存放的是编译器版本信息,如字符串“GCC:(GNU)4.2.0”。
.section debug
debug
段保存调试信息。
.section dynamic
dynamic
段保存动态连接信息。
.section hash
hash
段保存符号哈希表
.section line
line
段保存调试时的行号表,即源代码行号与编译后指令的对应表。
.section note
note
段保存额外的编译器信息,例如程序的发布者、版本号等信息。
.section strtab
strtab
段(string table)保存字符串表,用于存储ELF文件中用到的各种字符串。
.section symtab
symtab
段保存符号表
.section shstrtab
shstrtab
段(section string table)保存段名表。
.init
.finit
init
段和finit
段保存初始化与终结代码段。
.globl _start
.global _start
_start
是一个符号(Symbol),符号在汇编程序中代表一个地址,可以用在指令中,汇编程序经过汇编器的处理之后,所有的符号都被替换成它所代表的地址值。在C语言中我们通过变量名访问一个变量,其实就是读写某个地址的内存单元,我们通过函数名调用一个函数,其实就是跳转到该函数第一条指令所在的地址,所以变量名和函数名都是符号,本质上是代表内存地址的。
.globl
和global
指示告诉汇编器,_start
这个符号要被链接器用到,所以要在目标文件的符号表中标记它是一个全局符号。_start
就像C程序的main
函数一样特殊,是整个程序的入口,链接器在链接时会查找目标文件中的_start
符号代表的地址,把它设置为整个程序的入口地址,所以每个汇编程序都要提供一个_start
符号并且用.globl
声明。如果一个符号没有用.globl
声明,就表示这个符号不会被链接器用到。
这里定义了_start
符号,汇编器在翻译汇编程序时会计算每个数据对象和每条指令的地址,当看到这样一个符号定义时,就把它后面一条指令的地址作为这个符号所代表的地址。而_start
这个符号又比较特殊,它所代表的地址是整个程序的入口地址。
.weak symbol_name
.weak
伪操作用于设置符号的属性为弱,在汇编程序中,符号的默认属性为强(strong),如果此符号之前从未定义过,则同时创建此符号,并定义其属性为weak。
如果符号的属性为weak,那么它无需定义具体的内容。在链接的过程中,另外一个属性为strong的同名符号可以将此weak符号的内容强制覆盖。利用此特性,.weak
伪操作常用于预先预留一个空符号,使得其能够通过汇编器语法检查,但是在后续的程序中定义符号的真正实体,并且在链接阶段将空符号覆盖并链接。
.string "hello world!\n"
.string
伪操作将从当前PC地址处开始分配若干个字节空间用于存放“string”字符串。字节的个数取决于字符串的长度。
.asciz "string"
asciz
伪操作用于将从当前PC地址处开始分配若干个字节空间用于存放“string”字符串。相当于string
伪操作的别名。
.file filename
.file
伪操作指示汇编器该汇编程序的逻辑文件名。
.local symbol_name
.local
伪操作用于定义局部符号,使此符号不能够被其他程序文件可见。
.type type_name, %function
.type type_name, %object
.type
伪操作用于定义符号的类。即.type type_name, %function
表示将名为type_name的符号定义为一个函数(function).type type_name, %object
表示将名为type_name的符号定义为数据对象。
.zero integer
zero
伪操作将从当前PC处分配integer个字节空间,并用0填充。
.byte expression [, expression]
.2byte expression [, expression]
.4byte expression [, expression]
.8byte expression [, expression]
.half expression [, expression]
.word expression [, expression]
.dword expression [, expression]
.float expression [, expression]
.double expression [, expression]
.quad expression [, expression]
byte
伪操作将当前PC地址开始分配若干个字节(byte)的空间,每个字节的填充值由逗号分隔开的expression指定,[]
之间的值可省略。
2byte
伪操作将当前PC地址开始分配若干个双字节(2byte)的空间,每个双字节的填充值由逗号分隔开的expression指定,[]
之间的值可省略。分配的空间地址可以与双字节非对齐。
4byte
伪操作将当前PC地址开始分配若干个4字节(4byte)的空间,每个4字节的填充值由逗号分隔开的expression指定,[]
之间的值可省略。分配的空间地址可以与4字节非对齐。
8byte
伪操作将当前PC地址开始分配若干个8字节(8byte)的空间,每个8字节的填充值由逗号分隔开的expression指定,[]
之间的值可省略。分配的空间地址可以与8字节非对齐。
half
伪操作将当前PC地址开始分配若干个半字(half-word)的空间,每个半字的填充值由逗号分隔开的expression指定,[]
之间的值可省略。分配的空间地址一定与半字对齐。
word
伪操作用于将当前PC地址开始分配若干个字(word)的空间,每个字的填充值由逗号分隔开的expression指定。这里的空间分配一定是注意是按字对齐。
dword
伪操作将从当前PC处开始分配若干个双字(double word)的空间,每个双字填充的值由逗号分隔开的expression指定。这里的空间分配一定是双子对齐的。
float
伪操作将从当前PC处开始分配若干个单精度浮点数(32位)的空间,每个单精度浮点数填充的值由逗号分隔开的expression指定。空间分配的地址一定与32位对齐。
double
伪操作将从当前PC处开始分配若干个双精度浮点数(64位)的空间,每个单精度浮点数填充的值由逗号分隔开的expression指定。空间分配的地址一定与64位对齐。
quad
伪操作将从当前PC处开始分配8字节(64位)的空间,每个8字节的填充值由逗号分隔开的expression指定,[]
之间的值可省略。
.align integer
align
伪操作用于将当前PC值推进至2的integer次方字节对齐的位置。例:align 2
即表示将当前PC值推进至4字节对齐的位置。
.balign integer
balign
伪操作用于将当前PC值推进至integer字节对齐的位置。
.size main_size, .-main
size
伪操作用于指定符号的大小。.
表示当前位置,减去main符号的地址即为整个main函数的大小。相当于将将main函数的大小赋值给符号main_size。
.equ name,value
equ
伪操作用于进行常数的定义。
.set name,value
set
伪操作用于给一个全局变量或局部变量赋值,和.equ
的功能一样。
.option {rvc,norvc,push,pop,relax,norelax,pic,nopic}
option
伪操作用于设定某些架构特定的选项,使得汇编器能够识别并按照选项的定义采取相应的行为:
rvc
和norvc
是RSIC-V特有的选项,用于控制是否生成16位宽的压缩指令:
rvc
表示接下来的汇编程序可以被汇编生成16位宽的压缩指令。norvc
表示接下来的汇编程序可以被不可以被汇编生成16位宽的压缩指令。
push
和pop
用于临时性的保存或者恢复.option
伪操作指定的选项。
relax
和norelax
允许链接器松弛(linker relaxation,链接时多次扫描代码,尽可能将跳转两条指令替换为一条)和不允许链接器松弛1。
pic
和nopic
表示位置无关的代码和表示与位置有关的代码。
.comm name, length
.common name, length
comm
和common
伪操作用于声明一个名为name
的未初始化存储区间,区间大小为length字节。由于是未初始化存储区间,所以在链接时会链接至.bss
段。
.include "xxx_core.h"
include
伪指令用于包含指定的头文件。
.space size, fill
space
伪操作用于分配一段内存单元,用fill填充,若省略, file
则默认填充0
。
定义宏
.macro mac, a, b, c //定义一个宏,宏名为mac,参数为a,b,c
mul t0, \b, \c //mul指令将b和c相乘得到乘积写入to寄存器
add a, \t0, \a //累加结果存储写入a
.endm
类似于C语言的宏
macro
和endm
伪操作可以将一段代码定义为一个整体,称为宏指令。然后就可以在程序中通过宏指令多次调用该段代码。**注意:**先定义后使用。
参考《GNU手册》第7章《Assembler Directives》
🤔
🎡
🤷♀️
链接器松弛(linker relaxation):
跳转并链接指令(jump and link)中有20位的相对地址域,因此一条指令就足够跳到很远的位置。尽管编译器为每个外部函数的跳转都生成了两条指令,很多时候其实一条就已经足够了。从两条指令到一条的优化同时节省了时间和空间开销,因此链接器会扫描几遍代码,尽可能地把两条指令替换为一条。每次替换会导致 函数和调用它的位置之间的距离缩短,所以链接器会多次扫描替换,直到代码不再改变。这个过程称为链接器松弛,名字来源于求解方程组的松弛技术。除了过程调用之外,对于 gp指针 ±2KiB范围内的数据访问, RISC-V链接器也会 使用一个全局指针替换掉 lui和 auipc两条指令。对 tp指针 ±2KiB范围内的线程局部变量访问也有类似的处理。 ↩︎