ARM GNU汇编基础
0 前言
全文补充提醒:
笔者在阅读ARM官方文档及查阅实际的u-boot源码中的汇编代码后,发现了一些不同于ARM官方文档中的汇编语法,查阅相关资料后,才发现主要由于汇编器的不同,有两种不同的汇编语法:
ARM标准汇编
- 汇编程序:armasm
GNU ARM汇编
- 汇编程序:as
两者在语法上主要的区别在于伪操作的不同,其他相关的指令基本上是一致的,所以这一区别并不会对我们下文的学习造成太大的影响,为了方便,笔者通篇的示例均以GNU ARM汇编语法为标准,使用的汇编程序为arm-linux-gnueabihf-as.读者如需学习ARM标准汇编相关知识,需自行参考ARM官方文档 ARM Software Development Toolkit User Guide。
第二次补充提醒:
全文没有过多的去关注和介绍所有的ARM指令,更多的关注点均在基础语法和相关知识
全文内容大部分参考ARM官方文档 ARM Software Development Toolkit User Guide第五章以及GNU文档Using as,主要介绍了编写ARM和Thumb汇编程序的通用准则。主要包含了以下章节:
- ARM架构概述
- 汇编语言的模块结构
- 数据处理指令
- 内存访问指令
- 条件执行
- 宏的使用
在开始之前,希望读者能够搭建好相应的开发环境(编译工具链和运行环境),在此,笔者使用的是arm-linux-gnueabihf-交叉编译工具链,为了能够运行ARM程序,笔者使用的是QEMU,相关工具的下载和安装在此不再详述。
由于计算机最终只能识别机器码,也就是二进制序列,所以汇编语言同其他语言一样,需要相应的工具将其转换成机器语言,这就需要前面提的编译工具链,由于笔者的平台是X86架构,而编写的是ARM架构上的程序,因此需要一个能够执行ARM指令的设备,在这里笔者使用的即前文提及的QEMU工具,由于笔者编译的平台是X86架构,而目标机器是ARM架构,所以需要的是交叉编译工具链,不清楚这一知识的读者可自行补充交叉编译相关知识。
当你有一个写好的汇编代码文件( .s 后缀)后,你需要使用as工具进行汇编,生成机器语言,再使用ld工具进行链接。
$ arm-linux-gnueabihf-as sum10.s -o sum10.o -g
$ arm-linux-gnueabihf-ld sum10.o -o sum10
/*
-g:带有debug信息
-o:指定输出文件名
*/
Note:
c语言编译过程中,中途会先进行编译生成汇编代码,然后再进行汇编和链接,最终生成可执行文件
1 ARM架构概述
本章主要简单介绍下后文中所需要的一些ARM架构相关知识,读者如遇见不清楚的地方,可自行去阅读ARM Architectural Reference Manual。
ARM是一个典型的RISC(精简指令集)处理器,只有加载和存储指令可以访问内存,数据操作相关指令只能操作寄存器。这也就意味着程序更新一次内存中数据,至少需要三步:
- 从内存中将数据读取到寄存器中
- 对寄存器中的数据进行更新
- 将更新后的数据放回内存中
1.1 ARM 架构版本
ARM family | ARM架构版本 |
---|---|
ARM7 | ARM v4 |
ARM9 | ARM v5 |
ARM11 | ARM v6 |
Cortex-A | ARM v7-A |
Cortex-R | ARM v7-R |
Cortex-M | ARM v7-M |
1.2 ARM & Thumb state
在ARM v4T和v4TxM架构中,定义了一种长度为16bits的指令集,并称之为Thumb指令集,这一指令集是ARM指令集的一个子集。这一指令集同ARM指令集主要有以下区别:
- 在寄存器的访问上受一定的限制
- 只能通过分支指令实现条件执行
- 不允许访问桶式移位器(barrel shifter)
ARM处理器执行ARM指令时被称为处于ARM状态,执行Thumb指令时,被称为Thumb状态。
Note:后面会详细急介绍以上三点区别,读者不清楚,不用着急
ARM处理器最开始总是处于ARM状态,可通过BX指令转换至Thumb状态。
1.3 地址空间
在ARM v3架构之后,所有的的处理器均有32bit的寻址范围
1.4 处理器模式
ARM拥有以下7种基本操作模式:
- User
- FIQ
- IRQ
- Supervisor(svc)
- Abort
- Undefined
- System
以上其中模式中,大部分的程序都运行与User模式下,其他的六种均为特权模式
1.5 寄存器
ARM处理器一共提供了37个寄存器。寄存器排布在部分重叠的存储区域。每种模式下均有不同的寄存器组。
User mode | IRQ | FIQ | Undef | Abort | SVC |
---|---|---|---|---|---|
r0-r7 | |||||
r8 | r8 | ||||
r9 | r9 | ||||
r10 | r10 | ||||
r11 | r11 | ||||
r12 | r12 | ||||
r13 (sp) | r13 (sp) | r13 (sp) | r13 (sp) | r13 (sp) | r13 (sp) |
r14 (lr) | r14 (lr) | r14 (lr) | r14 (lr) | r14 (lr) | r14 (lr) |
r15 (pc) | |||||
cpsr | |||||
spsr | spsr | spsr | spsr | spsr |
30个32位通用寄存器,根据当前处理器的模式,可以随时看到其中的15个寄存器,分别是:r0,r1...r14。
通常情况下,r13会被用作栈指针(stack pointer),r14会被用作链接寄存器(link register),用于存储调用子程序时存储返回地址。r15是程序计数器,用于存放下一条需要执行的指令的位置,所以可以通过将程序需要跳转的地址放入PC来实现程序跳转。
CPSR寄存器(Current Program Status Register)主要持有了以下信息:
- ALU的状态标志信息(C/V/N/Z)
- 当前处理器的模式
- 中断Disable标志
- ARM State or Thumb State (如果该处理器支持Thumb)
SPSR寄存器(Saved Program Status Register)用于异常发生时存储CPSR。
CPSR寄存器:
N | Z | C | V | J | GE | E | A | I | F | T | M | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
31 | 30 | 29 | 28 | 24 | 19-16 | 9 | 8 | 7 | 6 | 5 | 4-0 |
M:处理器模式
T:ARM || Thumb 状态
N:负数标志
Z:结果为0
C:进位标志
V:溢出标志
1.6 ARM指令集概述
所有的ARM指令长度均为32bits。因为指令在内存中按字对齐存储,故地址最低2个bits应该为0。因此除了分支交换BX指令外,所有具有地址操作数的ARM指令都将忽略这两个bit,而BX正是通过这个bit来确定进入Thumb状态还是ARM状态。
ARM指令可大致分为以下积累几类:
- 分支指令
- 数据处理指令
- 状态寄存器访问指令
- 单一寄存器加载/存储指令
- 多寄存器加载/存储指令
- 信号量指令
- 协处理器指令
指令功能:
- 条件执行
- 寄存器访问
- 访问桶式移位器
1.7 Thumb指令集概述
所有的Thumb指令长度为16bits,且存储为半字对齐,故地址最低bit应该为0。因此除了分支交换BX指令外,所有具有地址操作数的指令都将忽略这个bit,而BX正是通过这个bit来确定进入Thumb状态还是ARM状态。
下面是Thumb指令与ARM指令的不同之处:
-
分支指令
- 相对于ARM指令,在范围上拥有更多的限制,而且仅支持无条件跳转
-
数据处理指令
- 受一定限制访的问r8-r15
- 一直会更新CPSR中的CVNZ位(ARM指令需添加S后缀),除了访问r8-r15
-
状态寄存器访问指令
- 没有相关的访问指令
-
单一寄存器加载/存储指令
- 不可访问r8-r15
-
多寄存器加载/存储指令
- 内存到寄存器的访问范围被限制在了r0-r7
- 此外,PUSH 和 POP可以分别使用r13和r14
-
信号量指令(无)
-
协处理器指令(无)
Thumb指令的功能
- 条件执行
- 只能通过分支指令来实现条件执行
- 访问寄存器
- 大部分情况下只能呢个访问r0-r7
- r8-r15访问受限,但也可以使用,比如作为快速临时存储
- 访问桶式移位器
- 只能通过特定的指令去访问,LSL、LSR、ASR、ROR
2 汇编语言结构
汇编语言在这里指的是允许通过ARM汇编程序分析和汇编生成目标代码的语言,它们可以是:
- ARM汇编语言
- Thumb汇编语言
- 以上两者的混合
2.1 汇编语言源文件的布局
汇编源文件中的的代码行的通常的形式如下:
GNU ARM 汇编格式:
{label:}{instruction | directive | pseudo-instruction} {@comment}
ARM 标准汇编格式:
{label} {instruction | directive | pseudo-instruction} {;comment}
{标签} {指令|伪操作|伪指令} {注释}
在这里需要注意的是,instruction | directive | pseudo-instruction 前面必须要有空格或者TAB
以上的三个部分均是可选的,也就是你完全可以用空行去分割你的代码,使其更具可读性
-
大小写规则
- 所有的指令助记符可以是大写或者小写,但是不能混合
- Directive必须大写
- 寄存器符号可以大写或者小写,但是不能混合
-
单行代码长度
- 为了使代码具有可读性,可允许使用‘’字符来分割换行
-
label 标签
- 在汇编语言中,标签是代表地址的一个符号,这个地址将在汇编期间被计算出来
- 在GNU汇编中,任何一个以冒号结尾的标识符都会被认为是一个标签,而不一定要在行首
-
局部标签 local labels
-
注释
- ARM 标准汇编的注释以 ; 开始
- GNU标准汇编的注释以@ 开始,同时也可以使用C语言中的 /* */
-
常量
- 数字
- 字符串
- 字符串常量用双引号“ ”括起来
- 布尔型
- {TRUE}
- {FALSE}
- 字符
- 使用单引号‘ ’
-
布尔型
2.2 ARM汇编的示例
2.2.1 example的汇编和链接
AREA ARMexample, CODE, READONLE
ENTRY
start mov r0, #10
mov r1, #3
add r0, r0, r1
stop mov r0, #0x18
ldr r1, =0x20026
swi 0x123456
end
;ARM 标准汇编语法,不做详细解释,读者可自行参考相关资料
GNU 汇编语法
.section .text
.global _start
_start:
MOV r0, #10
MOV r1, #3
ADD r0, r0, r1
stop:
MOV r0, #0x18
LDR r1, =0x20026
SWI 0x123456
将该文件保存为gnuAssembly.S,并使用as进行汇编,然后用ld进行链接生成可执行文件
arm-linux-gnueabihf-as -g gnuAsExample.S -o gnuAsExample.o
arm-linux-gnueabihf-ld gnuAsExample.o -o gnuAsExample
说明:
-g: 带有debug信息
-o:指定输出文件名
2.2.2 GDB和QEMU 调试
汇编链接生成gnuAsExample可执行文件之后,我们可以使用qemu-arm仿真工具执行该文件。
/*首先使用qemu-arm 加载并执行该可执行文件*/
qemu-arm -g 1234 gnuAsExample
/*另外开一个shell窗口,使用GDB工具进行联调*/
arm-linux-gnueabihf-gdb ./gnuAsExample
/*进入GDB模式后,执行如下命令*/
(gdb) target remote 127.0.0.1:1234
/*成功后,可看见如下信息*/
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
_start () at gnuAsExample.S:5
5 MOV r0, #10
说明:GDB工具连接的端口号需和使用qemu-arm中设置的端口号一致,在这里均为1234。详细的GDB命令在此不再详述,读者可自行查阅相关资料
2.2.3 示例详解
.section .text @声明text段
.global _start @声明全局变量
_start: @定义_start标签
MOV r0, #10 @立即数10赋值给r0
MOV r1, #3 @将立即数3赋值给r0
ADD r0, r0, r1 @将r0寄存器和r1寄存器的值相加,并将结果存放至r0
stop:
MOV r0, #0x18 @将0x18放入r0
LDR r1, =0x20026 @将0x20026放入r1
SWI 0x123456 @执行软中断命令
说明:stop标签后的三条指令是一个退出程序的指令序列,首先是设置r0和r1的值,然后再触发软件中断,这里面会用到者两个寄存器的值,具体这些值的含义读者可自行查阅相关资料,在此不进行说明,读者在这里也可以完全忽略具体的值,只需要对汇编的写法有个整体上的认知
在GNU ARM汇编中_start标签是默认的程序起始地址,且由于程序是通过加载器来加载的,因此必须将 _start符号声明为全局的,这样加载器才能找到。
2.3 Thumb 汇编示例
.section .text
.global _start
_start:
.code 32
adr r2, thumb+1
/*在前面我们有说到,由于ARM指令按字对齐,而Thumb指令按半字对齐,
因此有最低的一个bit或两个bit为0,而在这里我们通过加1,使其不为0,
下一条bx指令正是通过这一位判断是否需要进行ARM到Thumb状态的转换*/
bx r2
thumb:
.code 16
mov r0, #10
mov r1, #3
add r0, r0, r1
bkpt
下面是汇编链接调试过程,共读者参考
窗口 1
log@log:~$ arm-linux-gnueabihf-as -g gnuAsThumbEx.S -o gnuAsThumbEx.o
log@log:~$ arm-linux-gnueabihf-ld gnuAsThumbEx.o -o gnuAsThumbEx
log@log:~$ qemu-arm -g 1234 gnuAsThumbEx
窗口 2
log@log:~$ arm-linux-gnueabihf-gdb gnuAsThumbEx
GNU gdb (Linaro_GDB-2017.08) 8.0.0.20170823-git
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-unknown-linux-gnu --target=arm-linux-gnueabihf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from gnuAsThumbEx...done.
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
_start () at gnuAsThumbEx.S:7
7 adr r2, thumb+1
(gdb) info registers r2
r2 0x0 0
(gdb) n
8 bx r2
(gdb) info registers r2
r2 0x1005d 65629
(gdb) l
3 .global _start
4
5 _start:
6 .code 32
7 adr r2, thumb+1
8 bx r2
9
10 thumb:
11 .code 16
12 mov r0, #10
(gdb) n
thumb () at gnuAsThumbEx.S:12
12 mov r0, #10
(gdb) n
13 mov r1, #3
(gdb) info registers r2
r2 0x1005d 65629
(gdb) info registers r0
r0 0xa 10
(gdb) info registers r1
r1 0xf6fff66b -150997397
(gdb) n
14 add r0, r0, r1
(gdb) info registers r0 r1
r0 0xa 10
r1 0x3 3
(gdb) n
15 bkpt
(gdb) info registers r0 r1
r0 0xd 13
r1 0x3 3
(gdb) n
3 数据处理指令
ARM指令通常有一个或两个操作数,形如:
助记符{s}{条件} {rd} , 操作数1,操作数2
说明:
- {s}:指令加了s后缀后,意味着将会更新CPSR中的CVZN标志
- {条件}:可以决定该指令是否执行
- {rd}: 通常用于存储结果
- 操作数1:第一个操作数,可以是寄存器或立即数
- 操作数2:第二个操作数,可以是立即数或者是一个对寄存器进行移位操作
常见的表达式:
-
1 立即数
-
rx 寄存器x,r1、r2...
-
rx, asr n 算术右移n位
-
rx,lsl n 逻辑左移n位
-
rx,lsr n 逻辑右移n位
-
rx,ror n 循环右移n位
-
rx,rrx 循环右移1位
3.1 数据传输指令
-
mov:用于两个寄存器之间或者立即数和寄存器之间传递数据
-
mvn:和mov用法一致,区别在于会将第二个操作数进行按位取反后再进行传递
说明:
在这里特别需要说明的是使用mov和mvn在寄存器中和立即数中传递数据,这里的立即数是范围是有一定的限制的。
因为ARM指令编码格式长度微32位,抛去条件码、操作码、目标寄存器等,留给立即数的位数只剩下12位,因此它是没有办法去表示所有的32位的数的,如果按常规操作,它只能表示0x000-0xfff,但是在ARM中,并没有按常规操作去表示这些数据,而是将其中的低8位作为基数,高4位作为右移位数(x2):
例如:0x101代表的是:0x01 循环右移1×2位,变成:0x40000000
通过这种方法,使得12位的数可以表示比较大的数据,但是它依旧不肯呢个表示所有的数据,因此立即数是有一定的限制的,在编程时,如使用了非法立即数,汇编过程中会报告一个错误。
3.2 算数运算指令
- add:加法运算
- sub:减法运算
- rsb:反减运算
- adc:带进位的加法
- sbc:带进位的减法
- rsc:带进位的反减
3.3 逻辑运算指令
- and:与
- orr:或
- eor:异或
- bic:位清除操作
3.4 比较指令
- cmp:比较大小
- cmn:取反运算
- tst:按位与运算
- teq:按位异或运算
3.5 乘法指令
mvl、mla、umull、umlal、smull、smlal
3.6 CPSR访问指令
用于访问CPSR寄存器
- mrc:用于读出cpsr和spsr寄存器
- msr:用于写入cpsr和spsr寄存器
说明:CPSR是程序状态寄存器,只有一个,而SPSR寄存器在五种异常模式下各有一个,用于保存普通模式下的CPSR寄存器的值
4 内存访问指令
在ARM指令集中,只有特定的加载和存储指令允许访问内存,所以在对一个数据进行操作之前,都必须将该数加载到寄存器当中去,然后进行操作,操作完成后,再更新回内存中。
4.1 单一寄存器加载和存储
- ldr:可用于加载一个地址中的内容到寄存器中,也可以加载一个32bit长度的常量存储在寄存器中
- str:主要用与将寄存器中的值写入内存中
ldr是ARM中的一个指令,也同时是一个伪指令,其主要有两种用法:
-
加载一个常量到寄存器中
例如:ldr r1,=42
在这里,ldr是一个伪指令,汇编程序会根据需加载常量的不同,而翻译成不同的指令,具体规则是:
如果该常量能够使用mvn或mov指令进行操作,则直接将该指令翻译成mvn或mov
如果该常量并不能够直接被mvn或mov指令进行操作,则汇编器首先会在附近的文字池(literal pool)中存放这么一个常量,然后在使用ldr指令进行读取
literal pool: a portion of memory embedded in the code to hold constant value
在这里需要注意的是,编程者必须确保在LDR能够访问的范围内存在文字池,如果不存在,在进行汇编的过程中,会报告一个错误,这时编程者可通过 LTORG 指令放置一个文字池,详细信息见下文
放置文字池
通常来说,汇编程序会在每个area的末尾放置一个文字池,但是如果该area过大的话,可能会超出部分LDR指令能够访问的范围。
在ARM状态,从PC到常量的偏移量不能超过4KB
在Thumb状态,该偏移量不能超过1KB
.section .text .global _start _start: ldr r0, =3 ldr r1, =0x55555555 largeTable: .space 4200 .end
arm-linux-gnueabihf-as -g gnuAsLiteralPool.S -o gnuAsLiteralPool.o gnuAsLiteralPool.S: Assembler messages: gnuAsLiteralPool.S:6: 错误: invalid literal constant: pool needs to be closer
如上代码,在用as汇编时,会产生一个error,因为在ldr r1 , =0x55555555语句中,不能直接用mov或mvn加载,只能通过在文字池中存放该常量,然后使用ldr指令进行加载,然而在该示例中,因为后买你有一个4200字节的空间,使得文字池的范围超出了ldr指令可以访问的范围,因此需要我们手动在适合的位置放置一个文字池。如下:
.section .text .global _start _start: ldr r0, =3 ldr r1, =0x55555555 .ltorg largeTable: .space 4200 .end
在这里我们对汇编生成后的文件进行反汇编,结果如下:
log@log:~$ arm-linux-gnueabihf-objdump gnuAsLiteralPool.o -S gnuAsLiteralPool.o: 文件格式 elf32-littlearm Disassembly of section .text: 00000000 <_start>: .section .text .global _start _start: ldr r0, =3 0: e3a00003 mov r0, #3 ldr r1, =0x55555555 4: e51f1004 ldr r1, [pc, #-4] ; 8 <_start+0x8> 8: 55555555 .word 0x55555555 0000000c <largeTable>: ...
在这里可以看见,ldr r0,=3指令被翻译成了mov r0, #3;而ldr r1, =0x55555555,则被翻译成了两条指令,一个是在.word 0x55555555,一个是ldr r1, [pc, #-4] ; 8 <_start+0x8>。
-
加载一个地址
除了上一种用法,ldr更多的功能则是将某个特定地址中的内容加载进寄存器中,主要有以下用法
ldr r0,[r1] @将地址r1中的字数据加载进r0中 ldr r0,[r1,r2] @将存储器地址为r1+r2的字数据加载进r0中 ldr r0,[r1,#8] @将地址为r1+8的字数据加载进r0中 ldr r0,[r1],r2 @将r1地址中的字数据加载进r0中,并把r1+r2的值赋值给r1 ldr r0,[r1],#8 @将r1地址中的字数据加载进r0中,并把r1+8的值赋值给r1 ldr r0,[r1,r2]! @将存储器地址为r1+r2的字数据加载进r0中,同时把r1+r2赋值给r1 ldr r0,label @将label作为地址加载 ldr r0,=label @将label作为一个常量数进行加载
说明
除了上述ldr指令能够加载一个地址,同样也可以使用adr伪指令进行地址加载,这一伪指令是小范围的地址地址加载,主要通过相对于寄存器或当前PC的偏移量进行加载,且偏移量的范围是255字节(如果没有按字对齐)或者1020字节(如果地址按字对齐)
4.2 多寄存器加载和存储
ldm:用于从内存中读取连续的多个字存放进寄存器中
stm:用于将多个寄存器中的值存储到内存中去
ldm r0,{r4-r5} @将r0地址中的两个字内容分别加载到r4,r5
stm r1,{r4-r5} @将r4-r5两个寄存器的值写入r1地址中
后缀说明:
ia:每次传送后地址加4,其中的寄存器从左到右执行
ib:每次传送前地址加4,同上
da:每次传送后地址减4,其中的寄存器从右到左执行
db:每次传动前地址减4,同上
!:用最后的地址更新基地址
5 条件执行
在ARM状态,每个数据操作指令都可以通过添加‘s’后缀,根据操作结果去设置CPSR的ALU状态标志,也就是CPSR的N、C、V、Z位
在Thumb状态,没有s后缀,因为所有的指令都会自动去设置这一标志,除了通过mov和mvn指令去操作高位寄存器(r8-r15)
注意:在Thumb状态下,只能通过分支指令实现条件执行,所以下述通过添加指令后缀实现条件执行,都是处于ARM状态
5.1 ALU状态标志
CPSR寄存器包含了ALU以下状态标志:
- N:当操作结果为负数时,被置1
- Z:当操作结果为0时,被置1
- C:当操作结果出现进完位时,被置1
- V:当操作导致溢出时,被置1
当加减结果大于或等于2的32次方,或者由于一定而导致仅为,则C置1
当加减结果大于或等于2的31次方,或者少于-2的31次方,则发生溢出
不要将s后缀与cmpcmn st eq一起使用,因为这些指令默认都是会去更新这一标志的。
5.2 执行条件
后缀 | 标志 | 意义 |
---|---|---|
eq | Z = 1 | 相等 |
ne | Z = 0 | 不想等 |
cs/hs | C = 1 | unsigned >= |
cc/lo | C = 0 | unsigned < |
mi | N = 1 | 负数 |
pl | N = 0 | 0 或 正数 |
vs | V = 1 | 溢出 |
vc | V = 0 | 没有溢出 |
hi | c=1&&z=0 | unsigned > |
ls | c =0&&z=1 | unsigned <= |
ge | n = v | signed >= |
lt | n != v | signed < |
gt | z=0,n=v | signed > |
le | z=1,n!=v | signed <= |
6 宏的使用
在GNU汇编中的宏定义格式如下:
.macro 宏名 参数列表
宏体
.endm
example:
.macro FUN_ADD a,b
add a, a,
.endm
7 GNU ARM汇编常见伪操作
7.1 .section 伪操作
.section 伪操作格式如下:
.section section_name [,"flags" [, %type [, flag_specific_arguments]]]
在汇编中预置的一些段:
.text @代码段
.data@初始化数据段
.bss@未初始化数据段
.sdata
.sbss
.bss段应该在.text段之前
8 参考链接
ARM汇编基数:https://azeria-labs.com/writing-arm-assembly-part-1/