文章标题
2017-06-20 17:34
218 查看
一、引言
最近终于有时间把《X86汇编语言:从实模式到保护模式》这本书好好读了一遍,真是畅快!这本书从书名看,是一本讲x86汇编语言的书,但它真正的价值是一步一步带领读者进入操作系统的世界,写得非常的好,很佩服作者能够透过这本书将操作系统的原理讲清楚!
本人是搞嵌入式开发的,读完这本书后,我觉得完全可以按照此书的写作思路来写一本《从ARM汇编到操作系统》的书,虽然自己对ARM的掌握还很有限,但是我觉得值得尝试一下,一方面可以将自己这几年积累的知识总结一下,另一方面可以系统得学习一下ARM相关的知识。
二、硬件平台
由于手头有一块闲置的ARM9的板子,所以打算就用这块板子作为实践的平台。这块板子的配置为:
CPU:Atmel公司的At91SAM9260
SDRAM:
DataFlash:
NandFlash:
(1)启动
ARM上电执行第一条代码是从 0x0开始的,这是谁都不能改变的。但是0x0的地址可以通过很多方法进行映射,执行不同的代码。有些厂家在裸ARM芯片上增加了一小段启动代码,用以完成从特殊设备上加载代码,启动ARM。这种情况是非常多的,一些设计良好的ARM芯片,都可以从SPI Flash上加载,从UART上加载,大大的简化了ARM单板第一次烧写程序的复杂度。
AT91SAM9260是一款设计良好的ARM芯片。支持从NandFlash和DataFlash(SPI)上启动。从芯片手册上可知,DataFlash支持从SPI0口的NPCS0或者NPCS1启动,或者从其他设备启动。
AT91SAM9260的内存布局图:
芯片内部的存储空间:
芯片内部的映射图:
系统总是从地址0x0启动,芯片复位后,片内ROM被同时映射到地址0x0000_0000和0x0010_0000。REMAP(重映射)允许将芯片内部的SRAM0映射到地址0x0000_0000,如上图所示,当REMAP=1时,芯片内部的SRAM0映射到地址0x0000_0000。当REMAP=0时,BMS决定是从芯片内部ROM启动还是从EBI_NCS0片外存储启动。
当系统启动选择片内启动(BMS=1)时,程序上电后,内部ROM被映射在0x0000_0000地址,启动内部ROM的固化程序(Boot Program),将自动检测DataFlash或者NandFlash中前48个字节的数据,如果数据正确,则为有效代码,这时候系统自动将存在DataFlash或者NandFlash中的有效代码拷贝至SRAM0中去,接下来需要进行存储器的REMAP(即REMPA=1),经过REMAP后,SRAM0从映射前的0x0020_0000地址被映射到了0x0000_0000地址并且程序从此处开始执行。
当系统启动选择片外启动(BMS=0)时,程序上电后,EBI_NCS0(片选0)被映射在0x0000_0000地址,如果EBI_NCS0连接的是NorFlash,则可以直接从NorFlash启动。
内部ROM的固化程序Boot Program完成了如下工作:
三、ARM处理器
四、ARM处理器模式
ARM处理器在运行过程中可以在不同的处理模式间切换,但任一时刻只能处在一种模式下。在ARM11之前,ARM处理器一共有7种模式,从ARM11以后开始增加了第8种模式(Secure Mode)。处理器在运行代码时,会根据情况在7种模式间不停切换。对于Linux系统来说,只用到了usr、irq、svc、abt和und这5种模式。
7种模式具体如下:
(1)用户模式(User,usr):正常程序执行的模式
(2)快速中断模式(FIQ,fiq):用于高速数据传输和通道处理
(3)外部中断模式(IRQ,irq):用于通常的中断处理
(4)特权模式(Supervisor,svc):供操作系统使用的一种保护模式
(5)数据访问中止模式(Abort,abt):用于虚拟存储及存储保护
(6)未定义指令中止模式(Undefined,und):用于支持通过软件仿真硬件的协处理器
(7)系统模式(System,sys):用于运行特权级的操作系统任务
除了用户模式之外的其他6种处理器模式称为特权模式(Privileged Modes)。在这些模式下,程序可以访问所有的系统资源,也可以任意地进行处理器模式的切换。其中,除系统模式外,其他5种特权模式也称为异常模式。
处理器模式可以通过软件控制进行切换,也可以通过外部中断或异常处理过程进行切换。大多数的用户程序运行在用户模式下。此时,应用程序不能够访问一些受操作系统保护的系统资源。应用程序也不能直接进行处理器模式的切换。当需要进行处理器模式的切换时,应用程序可以产生异常处理,在异常处理过程中进行处理器模式的切换。这种体系结构可以使操作系统控制整个系统的资源。当应用程序发生异常中断时,处理器进入相应的异常模式。在每一种异常模式中都有一组寄存器,供相应的异常处理程序使用,这样就可以保证在进入异常模式时,用户模式下的寄存器(保存可程序运行状态)不被破坏。
系统模式并不是通过异常过程进入的,它和用户模式具有完全一样的寄存器。但是系统模式属于特权模式,能够访问所有的系统资源,也可以直接进行处理器模式的切换,它主要供操作系统任务使用。通常操作系统任务需要访问所有的系统资源,同时该任务仍然使用用户模式的寄存器组,而不是使用异常模式下相应的寄存器组,这样可以保证当异常中断发生时任务状态不被破坏。
五、ARM寄存器
ARM处理器共有37个寄存器,其中包括:
(1)31个通用寄存器,包括程序计数器(pc)在内。这些寄存器都是32位寄存器。
(2)6个状态寄存器,这些寄存器都是32位寄存器。
ARM处理器共有7种不同的处理器模式,在每一种处器模式下都有一组相应的寄存器组。任意时刻(也就是任意的处理器模式下),可见的寄存器包括15个通用寄存器(R0~R14)、一个或两个状态寄存器及程序计数器(pc)。在所有的寄存器中,有些是各模式共用的同一个物理寄存器;有一些寄存器是各模式自己拥有的独立的物理寄存器。
通用寄存器可以分为以下3类:
(1)未备份寄存器,包括R0~R7
对于每一个未备份寄存器来说,在所有的处理器模式下指的都是同一个物理寄存器。
(2)备份寄存器,包括R8~R14
对于备份寄存器R8~R12来说,每个寄存器对应两个不同的物理寄存器。当中断处理非常简单,仅使用R8~R12寄存器时,FIQ处理程序可以不必执行保存和恢复中断现场的指令,从而可以使中断处理过程非常迅速。
对于备份寄存器R13和R14来说,每个寄存器对应6个不同的物理寄存器,其中一个是用户模式和系统模式共用的,另外的5个对应于其他5种处理器模式。
(3)程序计数器(pc),即R15
上面的寄存器中,R0~R15是通用寄存器,可用来存储任意值;CPSR/SPSR是状态寄存器,用于获取ARM当前状态以及模式控制。
R13:常用作栈指针(sp),保存当前处理器模式的堆栈的栈顶地址
R14:链接寄存器(lr),在调用子程序时保存返回地址
R15:程序计数器(pc),处理器要取的下一条指令的地址
CPSR:通用状态寄存器
SPSR:备份状态寄存器(当中断或异常发生时保存CPSR,用户模式下没有SPSR)
六、ARM异常中断
当一个异常或中断发生时,ARM处理器会把程序计数器(pc)设置到一个特定的地址范围,然后从这些地址中加载指令执行;这一特定的地址范围称为中断/异常向量表(vector-table);
中断/异常向量表是由软件开发人员准备的,软件人员要负责在向量表中放置一系列跳转指令,跳转到专门处理某个异常或中断的子程序(这些子程序也由软件人员负责准备);
中断/异常向量表可以放置在低地址0x0000_0000处,或者高地址0xFFFF_0000处;当ARM处理器复位时,处理器采用物理地址,此时从地址0x0处查找异常向量表;当程序准备好页表并使能MMU后,处理器将采用虚拟地址,此时应当到地址0xFFFF_0000处查找异常向量表。
当一个异常或中断发生时,处理器会挂起正常的操作,转而从向量表的对应位置装载指令执行。
(1)异常/中断向量表的位置分布
(2)说明
复位(RESET):
处理器上电后执行的第一条指令的位置。这条指令将跳转到初始化代码。复位异常中断通常发生在如下情况下:
a、系统加电时
b、系统复位时
c、跳转到复位中断向量处执行,称为软复位
未定义指令(UNDEF):
处理器无法对一条指令译码时会产生异常,跳转到这里
软件中断(SWI):
处理器执行一条SWI指令后跳转到此处,此时,ARM核从非特权的USR模式切换到特权的SVC模式;
Linux从用户态到内核态的切换就是通过SWI指令实现的。用户应用程序使用的系统调用(如open,read,write)就是通过软件中断切换到内核中
预取指中止(PART):
处理器试图从一个未获得正确访问权限的地址取指令时发生
预取数据中止(DART):
处理器试图从一个未获得正确访问权限的地址取数据时发生
中断请求(IRQ):
当外设给处理器发来一个中断请求后,会跳转到此处
快速中断请求(FIQ):
外设可以将自己产生的中断设置为FIQ,比IRQ优先级高
七、ARM存储系统
(1)ARM的存储空间
ARM体系使用单一的平板地址空间。
a、该地址空间可以被看作是2^32个字节单元,这些字节单元的地址是一个无符号的32位数值,其取值范围为0~2^32-1。
b、该地址空间可以被看作是2^30个字单元,这些字单元的地址可以被4整除,也就是该地址的低两位为0。
c、该地址空间可以被看作是2^31个半字单元,这些半字单元的地址可以被2整除,也就是该地址的最低位为0。
(2)ARM的存储格式
ARM体系中的数据存储格式有两种:big-endian和little-endian,即大端和小端格式
(3)非对齐的存储访问操作
在ARM中,通常希望字单元的地址是字对齐的(地址低两位为0),半字单元的地址是半字对齐的(地址的最低位为0)。在存储访问操作中,如果存储单元的地址没有遵守上述的对齐规则,则称为非对齐的存储访问操作。
a、非对齐的指令预取操作
b、非对齐的数据访问操作
八、ARM汇编程序设计
(1)交叉编译工具
源文件需要经过编译才能生成可执行文件。PC上的编译工具链为gcc、ld、objcopy、objdump等,ARM平台上必须使用交叉编译工具arm-linux-gcc、arm-linux-ld等。
一个C/C++文件要经过如下4步才能变成可执行文件:
A、预处理:生成 .i文件,将要包含的文件插入原文件中、将宏定义展开、根据条件编译选择要使用的代码,使用arm-linux-cpp工具
B、编译:生成 .s文件(汇编代码),使用ccl工具
C、汇编:生成 .o文件(ELF目标文件,机器代码),使用arm-linux-as工具
D、连接:生成可执行文件,将生成的OBJ文件和系统库的OBJ文件、库文件连接起来,使用arm-linux-ld工具,被传递给连接器的文件,通常包括以下两种:
a、.o目标文件(OBJ文件)
b、.a归档库文件
1、arm-linux-gcc:
常用选项(区分大小写):
-c
完成预处理、编译和汇编,不执行连接,生成.o文件
-S
完成预处理和编译,不进行汇编和连接,生成.s文件
-E
完成预处理后停止,不执行后续动作,预处理后的代码直接在控制台输出
-o filename
指定输出的文件名,和上面的选项配套使用
-v
显示编译的详细过程(verbose)
-Wall
打开所有的警告信息
代码优化:
-O0: 不优化
-O1: 打开一定的优化选项
-O2: 除了不进行循环展开和函数内嵌,执行几乎所有的优化(常见)
-O3: 在O2的基础上增加了”-finline-functions”选项
-I/-L/-l
-I:增加头文件的搜索路径(默认为/usr/include等)
-L:增加库文件的搜索路径(默认为/usr/lib等)
-l:指明程序要链接到的库,可以是动态库(.so)或静态库(.a)
在/var/include下检索头文件
在/var/lib下检索库文件
Linux下的库文件都以lib开头,因此链接的库为libabc.so(如果没有动态库,则链接.a静态库)
-nostdlib
不连接系统标准启动文件和标准库文件,常用于编译uboot和内核。这些程序和普通的应用程序不同,不需要标准启动文件和库。
-static
链接静态库,而不是动态库。编译器会把整个的.a库加入应用程序
2、arm-linux-ld:
ld为链接器,用于将目标文件、库文件等链接成可执行的elf应用程序;arm-linux-gcc在生成a.out时会默认调用ld;编译较大型软件的常见用法是,由arm-linux-gcc生成多个.o文件,然后由arm-linux-ld统一生成一个可执行文件。
选项“-T”:可以直接使用它来指定代码段、数据段、BSS段的起始地址,也可以用来指定一个连接脚本,在脚本中进行更复杂的地址设置。
“-T”选项只用于连接Bootloader、内核等“没有底层软件支持”的软件;连接运行于操作系统之上的应用程序时,无需指定“-T”选项,它们使用默认的方式进行连接。
格式如下:
-Ttext startaddress
-Tdata startaddress
-Tbss startaddress
3、arm-linux-objcopy
被用来复制一个目标文件的内容到另一个文件中,可使用不同于源文件的格式来输出目标文件,即文件格式转换工具,常用于将elf格式的可执行应用程序转化为bin格式的程序。
4、arm-linux-objdump
用于显示二进制文件信息,常用于对.o文件或可执行文件进行反汇编。
常用选项:
-d
反汇编可执行段
-D
反汇编所有段
-f
显示文件的整体头部摘要信息
-h
显示文件中各个段的头部摘要信息
-m arm
指定反汇编时使用的架构
-b binary
指定要反汇编的文件格式
5、arm-linux-nm
用于显示.o或elf程序中的符号。
(2)ARM汇编语言程序格式
ARM汇编语言以段(section)为单位组织源代码。段是相对独立的、具有特定名称的、不可分割的指令或者数据序列。段又可以分为代码段和数据段,代码段存放执行代码,数据段存放代码执行时需要用到的数据。一个ARM源程序至少需要一个代码段,大的程序可以包含多个代码段和数据段。
ARM汇编语言源程序经过汇编处理后生成一个可执行的映像文件,该可执行的映像文件包含以下3部分:
a、一个或多个代码段,代码段通常是只读的
b、零个或多个包含初始值的数据段,这些数据段通常是可读可写的
c、零个或多个不包含初始值的数据段,这些数据段被初始化为0,通常是可读可写的
连接器根据一定的规则将各个段安排到内存中的相应位置。源程序中段之间的相邻关系与可执行映像文件中段之间的相邻关系并不一定相同。
(3)ARM指令集
A、ARM指令集的版本
ARM采用精简指令架构(RISC),其指令集目前已经发展到v8,从软件兼容性方面考虑,新的指令集一般主要增加一些DSP、多媒体处理等方面的功能,很少会修改或废弃旧的指令;但是ARM并不像INTEL一样严格保证向后兼容,这会导致一些老的程序不能在新的处理器上运行。
B、ARM指令的基本语法
ARM指令通常带有2个或3个操作数,例如加法指令
Rd:目标寄存器
Rn: 源寄存器1
Rm:源寄存器2
把存放在寄存器r1和r2中的值相加,然后把结果放到r3中。
C、指令的长度和书写方法
ARM指令编码后每条32位;为了节省内存空间,也可以编码为16位的thumb指令。
书写ARM汇编指令时,可不区分大小写,但建议格式统一,要么统一用大写,要么统一用小写。
D、注释方法
如果是使用arm公司提供的汇编器,则分号“;”后面的内容为注释;如果使用arm-linux-as,则将”@”后面的内容视为注释。
E、ARM-Thumb过程调用标准(ATPCS)
当一个项目中既包括用汇编语言写的源文件,也包括用C语言写的源文件时,就必然要涉及到汇编代码和C函数之间的调用问题;此时需要定义一套规范,以指明函数调用时如何传递参数,如何传递返回值等;针对这一要求,ARM提出了一套标准,称为ARM-Thumb过程调用标准(ATPCS),其中规定了如何通过寄存器来传递函数的参数和返回值。
ATPCS标准中最主要的规则如下:
a、一个函数中最前面的四个参数通过ARM的前4个寄存器r0、r1、r2和r3传递
b、从第5个参数开始,以后的参数通过递减满堆栈传递
c、函数返回的整型变量通过寄存器r0传递
函数参数传递图:
…
sp+16 参数8
sp+12 参数7
sp+8 参数6
sp+4 参数5
sp 参数4
r3 参数3
r2 参数2
r1 参数1
r0 参数0 返回值
r0-r3:用来存放函数的前4个参数,r0还用于保存函数的返回值
r4-r12: 可以用来分配局部变量,但使用前要通过堆栈保存
r13:栈指针
r14:函数调用时保存返回地址
r15:pc
函数调用的优化原则:尽量限制一个函数的参数,不要超过4个。
F、常用指令
1、数据处理指令:mov/mvn
最简单的ARM指令,执行的结果就是把一个立即数N送到目标寄存器Rd,N可以是通用寄存器,也可以是常量。语法:
mov:把一个32位数存入寄存器Rd,即Rd = N
mvn:把一个32位数按位取反后存入寄存器Rd
N为立即数,可以是一个寄存器Rn,也可以是一个使用”#”作为前缀的常量。指令可支持条件执行,可以附加S位。
2、算术指令:add/sub
用于实现32位有符号数或无符号数的加法和减法。语法:
add:32位加法,Rd=Rn+N
sub:32位减法,Rd=Rn-N
N为立即数,可以是一个寄存器Rm,也可以是一个使用”#”作为前缀的常量,还可以包括移位
3、逻辑指令
对2个源操作数进行按位的逻辑操作,结果存储到目标寄存器中。语法:
and:32位逻辑与;Rd=Rn & N
orr:32位逻辑或;Rd=Rn | N
eor:32位逻辑异或;Rd=Rn ^ N
bic:32位逻辑位清除;Rd=Rn &~ N
N可以是寄存器,也可以是常量,还可以包括移位。
bic指令通常用于清除寄存器的某个特定位,比如清除CPSR寄存器的I位和F位来使能中断;eor指令常用于反转某些位。
4、比较指令
比较指令用于对一个寄存器中的值和一个立即数进行比较或测试。比较指令根据结果更新cpsr的标志位,但不影响其它的寄存器。当比较指令改变了标志位后,后续指令就可以通过条件执行来改变程序的执行流程。比较指令不使用S后缀就可以改变标志位。语法:
cmp:比较(根据Rn-N设置标记位)
teq:等值比较(根据Rn^N设置标记位)
tst:位测试(根据Rn&N设置标记位)
5、分支指令(branch)
分支指令可以改变程序的执行流程或者调用子程序。这种指令使得程序可以实现子程序调用,if-else结构以及循环等;执行流程的改变会迫使程序计数器pc指向一个新的地址。语法:
b:跳转,pc=label
bl:带返回的跳转,pc=label,lr=bl指令后面一条指令的地址
b指令一般用于实现C代码中的if-else分支,或者for/while循环;bl指令一般用于实现函数调用/返回。
标号放在一行的开始处,后面加冒号。
带条件的跳转bleq
在一个.s文件中,字母开头的标号视为全局标号,是不能重名的;而纯数字的标号认为是局部标号,可以重名。
6、单寄存器load-store指令
load-store指令用于在内存和通用寄存器之间交换数据;ARM是不能直接操作内存中的数据的,必须首先把数据用load指令从内存读取到通用寄存器中,然后才能进行计算,最后再用store指令将运算的结果写回内存。语法:
ldr:把一个word(32位)从内存读入通用寄存器Rd
str:把一个通用寄存器的内容(32位)写入内存
如果需要读写16位数据,可以用ldrh/strh
如果需要读写8位数据,可以用ldrb/strb
address是内存中的地址,可以直接写一个标号;也可以先把基地址存到寄存器中,然后采用[基地址+/-偏移量]的方式访问内存。偏移量也可以有多种表示,可以直接用常数作为偏移量,也可以用另一个寄存器做偏移量。
ldr和str需要装载和存储地址对齐的数据,如ldr只能从0、4、8等地址装载32位的word。
例1:
例2:
ldr/str指令的寻址方式:
基地址寻址:
基地址加偏移寻址:
基地址加偏移并更新基地址:
注意:!的作用是在指令执行完毕后,更新基地址;
只更新基地址:
基于标号寻址:
在汇编中,标号实际上也是地址,因此可以直接在ldr/str指令后面使用标号。
7、多寄存器传输指令ldm/stm
为了提高效率,ARM还提供了和内存单元交换多个word的指令,这种指令常用于数据拷贝,入栈出栈等操作;多寄存器传输指令是执行时间最长的ARM指令。语法:
ldm:从内存中取出数据装载到多个寄存器中
stm:将多个寄存器中的内容保存到内存中
Rn中保存要访问的内存单元的基地址
!的作用是在指令执行完毕后,更新基地址
Regs是指令涉及的通用寄存器列表
^可以将spsr寄存器的值赋给cpsr
ldm/stm指令的寻址模式:IA/IB/DA/DB
IA(执行后增加,Increase After)
IB(执行前增加,Increase Before)
DA(执行后减少,Decrease After)
DB(执行前减少,Decrease Before)
stm/ldm指令常用于栈的操作以及数据拷贝:
入栈和出栈:
C代码默认使用stmdb和ldmia的组合
数据拷贝:
8、软件中断指令swi/svc
中断是由硬件产生的异常,而软件中断是通过执行特定的指令产生的异常。ARM定义了指令swi(ARMv7后更名为svc)来产生一个软件中断异常;当处理器执行完swi指令后,不会继续执行swi后面的指令,而是首先将CPU切换到svc模式,然后将pc设定为异常向量表基地址+0x8,并从此处继续执行。swi/svc指令的最主要工作就是将cpu从非特权的usr模式(对应linux的用户态)提升到特权的svc模式(对应Linux的内核态)。在Linux用户态执行的open/read等系统调用,在ARM平台上就是通过swi指令实现的。通常在用户模式下调用swi,从而使处理器从非特权模式(usr)切换到特权模式(svc)。
语法:
指令后面的数字为系统调用号,最大为0xffffff。系统调用号直接编译到指令代码中。
执行swi指令后,处理器的执行步骤如下:
–> 在lr_svc寄存器中记录swi指令后面一条指令的地址
–> 将cpsr的值(执行swi指令时所处的处理器状态)存入spsr_svc
–> pc = 异常向量表基地址 + 0x8
–> 将cpu切换到svc模式
–> cpsr_I = 1(屏蔽IRQ中断)
执行编号为123的系统调用:
软件中断的处理函数,应该从异常向量表的0x8单元处跳转过来:
在重新装载pc的时候,除非明确要求,否则不会把spsr中的内容恢复到cpsr中去。要恢复cpsr的方法是在寄存器列表后跟随一个”^”。
9、状态寄存器访问指令mrs/msr
要读写ARM的状态寄存器cpsr和spsr,需要使用两条特殊的指令:mrs和msr。语法:
mrs:将状态寄存器的内容读入通用寄存器Rd
msr:设定状态寄存器或其中的某个域
field:用msr指令写cpsr/spsr时,可以附加field标志从而只影响cpsr/spsr的部分位
支持的field标志有:
c – control域(bit[7:0])
x – extension域(bit[15:8])
s – status域(bit[23:16])
f – flags域(bit[31:24])
使能IRQ:
10、协处理器访问指令
ARM提供了专门的协处理器访问指令,协处理器可提供附加的计算能力,如浮点数运算、多媒体数据运算等,也可用于控制cache和MMU等,协处理器访问指令的格式必须参考对应的ARM手册。语法:
mrc:把数据从协处理器内部的寄存器读入ARM的通用寄存器
mcr:把通用寄存器中的数据写入协处理器内部的寄存器
cp:协处理器的编号,范围p0~p15
Rd:ARM中的通用寄存器,范围r0~r12
opcode1:操作码1,范围0~7
opcode2:操作码2,范围0~7
Cn:协处理器中的主寄存器,范围c0~c15
Cm:协处理器中的辅助寄存器,范围c0~c15
注意!op1, op2, Cn, Cm组合在一起代表特定的操作,其组合方式只能参考对应的手册。
11、ARM的伪指令
为了满足编程的需要,ARM还提供了一些伪指令。伪指令的使用方式和前面的指令一样,但汇编器(as)在汇编时,会首先根据具体情况把伪指令转换为前面介绍过的真正的ARM指令,然后再进行编码。
(1)压栈和出栈:
为了更好地使用堆栈,ARM根据栈的不同使用状态,提供了专门的入栈/出栈指令。这些指令会被汇编器转换为前面介绍的stm/ldm系列指令。
堆栈的分类:
从栈指针的生长方向上来划分,栈可以分为递增堆栈和递减堆栈
从栈指针指向的单元是否被使用上来划分,栈可以分为满堆栈和空堆栈
根据这两种分类,栈的使用可分为4种:
A. 递增满堆栈(FA, full ascend)
B. 递减满堆栈(FD, full descend)
C. 递增空堆栈(EA, empty ascend)
D. 递减空堆栈(ED, empty descend)
在Uboot或Linux中,默认采用递减满的方式使用堆栈。
递减满堆栈:
(2)常量和地址装载:
ARM指令采用32位编码,由于操作码要占用空间,因此不可能在一条指令中编码一个32位的立即数,为方便编程,ARM增加了一条伪指令,用于把一个32位的立即数存入寄存器。ARM还提供了一条伪指令,用于获得当前的运行地址。
语法:
ldr:常量装载伪指令,Rd=32位常量
adr:地址装载伪指令,Rd=32位相对地址
例1:
例2:
例3:
12、gcc编译器支持的伪汇编指令
在ARM汇编语言程序设计中,有一些特殊指令助记符,这些助记符与ARM指令不同,没有相对应的操作码,通常称这些特殊指令助记符为伪汇编指令或伪指令。伪指令类似于C语言中的include,define等,仅在程序的汇编过程中起作用,一旦汇编结束,伪指令的使命就完成。
伪汇编是编译器定义的,如果不是使用gcc,而是armcc,则编译器支持不同格式的伪汇编。
常用的伪汇编指令:
(1)标号(label)
gcc中的标号通过后面带的冒号来识别,例如:
(2).global/.globl
将symbol定义为全局标号,例如:
(3).byte
声明一个字节(8bit),相当于c语言中的char,例如:
(4).hword/.short
声明一个半字(16bit),相当于c语言中的short,例如:
(5).word/.int/.long
声明一个字(32bit),相当于C语言中的int,例如:
(6).ascii
声明一个字符串(非零结束符),例如:
(7).asciz/.string
声明一个字符串(以0为结束符),例如:
(8).fill
语法:
数据填充,次数为repeat次,每次填充size个字节,填充内容为value,size缺省为1, value缺省为0。例如:
(9).space
语法:
用value填充size个字节,value缺省为0。例如:
(10).arm/.code 32
定义以下代码使用ARM指令集编译。
(11).include
包含文件,可以包含头文件或其他汇编文件
更常见的头文件包含方式为:
(12).align/.balign
语法:
以alignment个字节对齐。
(13).end
标记汇编文件的结束行,即标号后的代码不作处理。
(14).section
定义elf文件中包含的段,可以是.text、.data、.bss等。
(15).text/.data/.bss
将符号后面的代码或数据编译到指定段中。
(16).if/.elseif/.else/.endif
条件编译,例如:
(17).ifdef
若symbol已定义,编译以下代码:
(18).ifndef
若symbol没定义,编译以下代码:
(19).rept/.endr
重复编译.rept与.endr之间的语句count次,例如:
(20).equ/.equiv
将符号替换为表达式的结果,类似于c中的define,例如:
(21).req
为ARM的通用寄存器起个名字,语法:
例如:
最近终于有时间把《X86汇编语言:从实模式到保护模式》这本书好好读了一遍,真是畅快!这本书从书名看,是一本讲x86汇编语言的书,但它真正的价值是一步一步带领读者进入操作系统的世界,写得非常的好,很佩服作者能够透过这本书将操作系统的原理讲清楚!
本人是搞嵌入式开发的,读完这本书后,我觉得完全可以按照此书的写作思路来写一本《从ARM汇编到操作系统》的书,虽然自己对ARM的掌握还很有限,但是我觉得值得尝试一下,一方面可以将自己这几年积累的知识总结一下,另一方面可以系统得学习一下ARM相关的知识。
二、硬件平台
由于手头有一块闲置的ARM9的板子,所以打算就用这块板子作为实践的平台。这块板子的配置为:
CPU:Atmel公司的At91SAM9260
SDRAM:
DataFlash:
NandFlash:
(1)启动
ARM上电执行第一条代码是从 0x0开始的,这是谁都不能改变的。但是0x0的地址可以通过很多方法进行映射,执行不同的代码。有些厂家在裸ARM芯片上增加了一小段启动代码,用以完成从特殊设备上加载代码,启动ARM。这种情况是非常多的,一些设计良好的ARM芯片,都可以从SPI Flash上加载,从UART上加载,大大的简化了ARM单板第一次烧写程序的复杂度。
AT91SAM9260是一款设计良好的ARM芯片。支持从NandFlash和DataFlash(SPI)上启动。从芯片手册上可知,DataFlash支持从SPI0口的NPCS0或者NPCS1启动,或者从其他设备启动。
AT91SAM9260的内存布局图:
芯片内部的存储空间:
芯片内部的映射图:
系统总是从地址0x0启动,芯片复位后,片内ROM被同时映射到地址0x0000_0000和0x0010_0000。REMAP(重映射)允许将芯片内部的SRAM0映射到地址0x0000_0000,如上图所示,当REMAP=1时,芯片内部的SRAM0映射到地址0x0000_0000。当REMAP=0时,BMS决定是从芯片内部ROM启动还是从EBI_NCS0片外存储启动。
当系统启动选择片内启动(BMS=1)时,程序上电后,内部ROM被映射在0x0000_0000地址,启动内部ROM的固化程序(Boot Program),将自动检测DataFlash或者NandFlash中前48个字节的数据,如果数据正确,则为有效代码,这时候系统自动将存在DataFlash或者NandFlash中的有效代码拷贝至SRAM0中去,接下来需要进行存储器的REMAP(即REMPA=1),经过REMAP后,SRAM0从映射前的0x0020_0000地址被映射到了0x0000_0000地址并且程序从此处开始执行。
当系统启动选择片外启动(BMS=0)时,程序上电后,EBI_NCS0(片选0)被映射在0x0000_0000地址,如果EBI_NCS0连接的是NorFlash,则可以直接从NorFlash启动。
内部ROM的固化程序Boot Program完成了如下工作:
三、ARM处理器
四、ARM处理器模式
ARM处理器在运行过程中可以在不同的处理模式间切换,但任一时刻只能处在一种模式下。在ARM11之前,ARM处理器一共有7种模式,从ARM11以后开始增加了第8种模式(Secure Mode)。处理器在运行代码时,会根据情况在7种模式间不停切换。对于Linux系统来说,只用到了usr、irq、svc、abt和und这5种模式。
7种模式具体如下:
(1)用户模式(User,usr):正常程序执行的模式
(2)快速中断模式(FIQ,fiq):用于高速数据传输和通道处理
(3)外部中断模式(IRQ,irq):用于通常的中断处理
(4)特权模式(Supervisor,svc):供操作系统使用的一种保护模式
(5)数据访问中止模式(Abort,abt):用于虚拟存储及存储保护
(6)未定义指令中止模式(Undefined,und):用于支持通过软件仿真硬件的协处理器
(7)系统模式(System,sys):用于运行特权级的操作系统任务
除了用户模式之外的其他6种处理器模式称为特权模式(Privileged Modes)。在这些模式下,程序可以访问所有的系统资源,也可以任意地进行处理器模式的切换。其中,除系统模式外,其他5种特权模式也称为异常模式。
处理器模式可以通过软件控制进行切换,也可以通过外部中断或异常处理过程进行切换。大多数的用户程序运行在用户模式下。此时,应用程序不能够访问一些受操作系统保护的系统资源。应用程序也不能直接进行处理器模式的切换。当需要进行处理器模式的切换时,应用程序可以产生异常处理,在异常处理过程中进行处理器模式的切换。这种体系结构可以使操作系统控制整个系统的资源。当应用程序发生异常中断时,处理器进入相应的异常模式。在每一种异常模式中都有一组寄存器,供相应的异常处理程序使用,这样就可以保证在进入异常模式时,用户模式下的寄存器(保存可程序运行状态)不被破坏。
系统模式并不是通过异常过程进入的,它和用户模式具有完全一样的寄存器。但是系统模式属于特权模式,能够访问所有的系统资源,也可以直接进行处理器模式的切换,它主要供操作系统任务使用。通常操作系统任务需要访问所有的系统资源,同时该任务仍然使用用户模式的寄存器组,而不是使用异常模式下相应的寄存器组,这样可以保证当异常中断发生时任务状态不被破坏。
五、ARM寄存器
ARM处理器共有37个寄存器,其中包括:
(1)31个通用寄存器,包括程序计数器(pc)在内。这些寄存器都是32位寄存器。
(2)6个状态寄存器,这些寄存器都是32位寄存器。
ARM处理器共有7种不同的处理器模式,在每一种处器模式下都有一组相应的寄存器组。任意时刻(也就是任意的处理器模式下),可见的寄存器包括15个通用寄存器(R0~R14)、一个或两个状态寄存器及程序计数器(pc)。在所有的寄存器中,有些是各模式共用的同一个物理寄存器;有一些寄存器是各模式自己拥有的独立的物理寄存器。
通用寄存器可以分为以下3类:
(1)未备份寄存器,包括R0~R7
对于每一个未备份寄存器来说,在所有的处理器模式下指的都是同一个物理寄存器。
(2)备份寄存器,包括R8~R14
对于备份寄存器R8~R12来说,每个寄存器对应两个不同的物理寄存器。当中断处理非常简单,仅使用R8~R12寄存器时,FIQ处理程序可以不必执行保存和恢复中断现场的指令,从而可以使中断处理过程非常迅速。
对于备份寄存器R13和R14来说,每个寄存器对应6个不同的物理寄存器,其中一个是用户模式和系统模式共用的,另外的5个对应于其他5种处理器模式。
(3)程序计数器(pc),即R15
上面的寄存器中,R0~R15是通用寄存器,可用来存储任意值;CPSR/SPSR是状态寄存器,用于获取ARM当前状态以及模式控制。
R13:常用作栈指针(sp),保存当前处理器模式的堆栈的栈顶地址
R14:链接寄存器(lr),在调用子程序时保存返回地址
R15:程序计数器(pc),处理器要取的下一条指令的地址
CPSR:通用状态寄存器
SPSR:备份状态寄存器(当中断或异常发生时保存CPSR,用户模式下没有SPSR)
六、ARM异常中断
当一个异常或中断发生时,ARM处理器会把程序计数器(pc)设置到一个特定的地址范围,然后从这些地址中加载指令执行;这一特定的地址范围称为中断/异常向量表(vector-table);
中断/异常向量表是由软件开发人员准备的,软件人员要负责在向量表中放置一系列跳转指令,跳转到专门处理某个异常或中断的子程序(这些子程序也由软件人员负责准备);
中断/异常向量表可以放置在低地址0x0000_0000处,或者高地址0xFFFF_0000处;当ARM处理器复位时,处理器采用物理地址,此时从地址0x0处查找异常向量表;当程序准备好页表并使能MMU后,处理器将采用虚拟地址,此时应当到地址0xFFFF_0000处查找异常向量表。
当一个异常或中断发生时,处理器会挂起正常的操作,转而从向量表的对应位置装载指令执行。
(1)异常/中断向量表的位置分布
异常/中断 地址 复位 0x0 未定义指令 0x4或0xffff,0004 软件中断(SWI) 0x8或0xffff,0008 取指令异常 0xc或0xffff,000c 取数据异常 0x10或0xffff,0010 保留 0x14或0xffff,0014 中断IRQ 0x18或0xffff,0018 快速中断FIQ 0x1c或0xffff,001c
(2)说明
复位(RESET):
处理器上电后执行的第一条指令的位置。这条指令将跳转到初始化代码。复位异常中断通常发生在如下情况下:
a、系统加电时
b、系统复位时
c、跳转到复位中断向量处执行,称为软复位
未定义指令(UNDEF):
处理器无法对一条指令译码时会产生异常,跳转到这里
软件中断(SWI):
处理器执行一条SWI指令后跳转到此处,此时,ARM核从非特权的USR模式切换到特权的SVC模式;
Linux从用户态到内核态的切换就是通过SWI指令实现的。用户应用程序使用的系统调用(如open,read,write)就是通过软件中断切换到内核中
预取指中止(PART):
处理器试图从一个未获得正确访问权限的地址取指令时发生
预取数据中止(DART):
处理器试图从一个未获得正确访问权限的地址取数据时发生
中断请求(IRQ):
当外设给处理器发来一个中断请求后,会跳转到此处
快速中断请求(FIQ):
外设可以将自己产生的中断设置为FIQ,比IRQ优先级高
七、ARM存储系统
(1)ARM的存储空间
ARM体系使用单一的平板地址空间。
a、该地址空间可以被看作是2^32个字节单元,这些字节单元的地址是一个无符号的32位数值,其取值范围为0~2^32-1。
b、该地址空间可以被看作是2^30个字单元,这些字单元的地址可以被4整除,也就是该地址的低两位为0。
c、该地址空间可以被看作是2^31个半字单元,这些半字单元的地址可以被2整除,也就是该地址的最低位为0。
(2)ARM的存储格式
ARM体系中的数据存储格式有两种:big-endian和little-endian,即大端和小端格式
(3)非对齐的存储访问操作
在ARM中,通常希望字单元的地址是字对齐的(地址低两位为0),半字单元的地址是半字对齐的(地址的最低位为0)。在存储访问操作中,如果存储单元的地址没有遵守上述的对齐规则,则称为非对齐的存储访问操作。
a、非对齐的指令预取操作
b、非对齐的数据访问操作
八、ARM汇编程序设计
(1)交叉编译工具
源文件需要经过编译才能生成可执行文件。PC上的编译工具链为gcc、ld、objcopy、objdump等,ARM平台上必须使用交叉编译工具arm-linux-gcc、arm-linux-ld等。
一个C/C++文件要经过如下4步才能变成可执行文件:
A、预处理:生成 .i文件,将要包含的文件插入原文件中、将宏定义展开、根据条件编译选择要使用的代码,使用arm-linux-cpp工具
B、编译:生成 .s文件(汇编代码),使用ccl工具
C、汇编:生成 .o文件(ELF目标文件,机器代码),使用arm-linux-as工具
D、连接:生成可执行文件,将生成的OBJ文件和系统库的OBJ文件、库文件连接起来,使用arm-linux-ld工具,被传递给连接器的文件,通常包括以下两种:
a、.o目标文件(OBJ文件)
b、.a归档库文件
1、arm-linux-gcc:
常用选项(区分大小写):
-c
完成预处理、编译和汇编,不执行连接,生成.o文件
-S
完成预处理和编译,不进行汇编和连接,生成.s文件
-E
完成预处理后停止,不执行后续动作,预处理后的代码直接在控制台输出
-o filename
指定输出的文件名,和上面的选项配套使用
$>arm-linux-gcc hello.c -S -o hello.s $>arm-linux-gcc hello.c -E -o hello.i
-v
显示编译的详细过程(verbose)
-Wall
打开所有的警告信息
代码优化:
-O0: 不优化
-O1: 打开一定的优化选项
-O2: 除了不进行循环展开和函数内嵌,执行几乎所有的优化(常见)
-O3: 在O2的基础上增加了”-finline-functions”选项
-I/-L/-l
-I:增加头文件的搜索路径(默认为/usr/include等)
-L:增加库文件的搜索路径(默认为/usr/lib等)
-l:指明程序要链接到的库,可以是动态库(.so)或静态库(.a)
$>arm-linux-gcc -I/var/include/ -L/var/lib/ -labc -o hello hello.c
在/var/include下检索头文件
在/var/lib下检索库文件
Linux下的库文件都以lib开头,因此链接的库为libabc.so(如果没有动态库,则链接.a静态库)
-nostdlib
不连接系统标准启动文件和标准库文件,常用于编译uboot和内核。这些程序和普通的应用程序不同,不需要标准启动文件和库。
-static
链接静态库,而不是动态库。编译器会把整个的.a库加入应用程序
$>arm-linux-gcc -static -o hello hello.c $>file hello
2、arm-linux-ld:
ld为链接器,用于将目标文件、库文件等链接成可执行的elf应用程序;arm-linux-gcc在生成a.out时会默认调用ld;编译较大型软件的常见用法是,由arm-linux-gcc生成多个.o文件,然后由arm-linux-ld统一生成一个可执行文件。
选项“-T”:可以直接使用它来指定代码段、数据段、BSS段的起始地址,也可以用来指定一个连接脚本,在脚本中进行更复杂的地址设置。
“-T”选项只用于连接Bootloader、内核等“没有底层软件支持”的软件;连接运行于操作系统之上的应用程序时,无需指定“-T”选项,它们使用默认的方式进行连接。
格式如下:
-Ttext startaddress
-Tdata startaddress
-Tbss startaddress
3、arm-linux-objcopy
被用来复制一个目标文件的内容到另一个文件中,可使用不同于源文件的格式来输出目标文件,即文件格式转换工具,常用于将elf格式的可执行应用程序转化为bin格式的程序。
$>arm-linux-objcopy -O binary led led.bin -O binary指明输出bin格式的应用程序,最后面的两个参数分别为input-file和output-file
4、arm-linux-objdump
用于显示二进制文件信息,常用于对.o文件或可执行文件进行反汇编。
常用选项:
-d
反汇编可执行段
-D
反汇编所有段
-f
显示文件的整体头部摘要信息
-h
显示文件中各个段的头部摘要信息
-m arm
指定反汇编时使用的架构
-b binary
指定要反汇编的文件格式
$>arm-linux-objdump -D led01 $>arm-linux-objdump -D -b binary -m arm led01.bin
5、arm-linux-nm
用于显示.o或elf程序中的符号。
$>arm-linux-nm hello
(2)ARM汇编语言程序格式
ARM汇编语言以段(section)为单位组织源代码。段是相对独立的、具有特定名称的、不可分割的指令或者数据序列。段又可以分为代码段和数据段,代码段存放执行代码,数据段存放代码执行时需要用到的数据。一个ARM源程序至少需要一个代码段,大的程序可以包含多个代码段和数据段。
ARM汇编语言源程序经过汇编处理后生成一个可执行的映像文件,该可执行的映像文件包含以下3部分:
a、一个或多个代码段,代码段通常是只读的
b、零个或多个包含初始值的数据段,这些数据段通常是可读可写的
c、零个或多个不包含初始值的数据段,这些数据段被初始化为0,通常是可读可写的
连接器根据一定的规则将各个段安排到内存中的相应位置。源程序中段之间的相邻关系与可执行映像文件中段之间的相邻关系并不一定相同。
(3)ARM指令集
A、ARM指令集的版本
ARM采用精简指令架构(RISC),其指令集目前已经发展到v8,从软件兼容性方面考虑,新的指令集一般主要增加一些DSP、多媒体处理等方面的功能,很少会修改或废弃旧的指令;但是ARM并不像INTEL一样严格保证向后兼容,这会导致一些老的程序不能在新的处理器上运行。
B、ARM指令的基本语法
ARM指令通常带有2个或3个操作数,例如加法指令
add Rd, Rn, Rm
Rd:目标寄存器
Rn: 源寄存器1
Rm:源寄存器2
add r3, r1, r2
把存放在寄存器r1和r2中的值相加,然后把结果放到r3中。
C、指令的长度和书写方法
ARM指令编码后每条32位;为了节省内存空间,也可以编码为16位的thumb指令。
书写ARM汇编指令时,可不区分大小写,但建议格式统一,要么统一用大写,要么统一用小写。
D、注释方法
如果是使用arm公司提供的汇编器,则分号“;”后面的内容为注释;如果使用arm-linux-as,则将”@”后面的内容视为注释。
E、ARM-Thumb过程调用标准(ATPCS)
当一个项目中既包括用汇编语言写的源文件,也包括用C语言写的源文件时,就必然要涉及到汇编代码和C函数之间的调用问题;此时需要定义一套规范,以指明函数调用时如何传递参数,如何传递返回值等;针对这一要求,ARM提出了一套标准,称为ARM-Thumb过程调用标准(ATPCS),其中规定了如何通过寄存器来传递函数的参数和返回值。
ATPCS标准中最主要的规则如下:
a、一个函数中最前面的四个参数通过ARM的前4个寄存器r0、r1、r2和r3传递
b、从第5个参数开始,以后的参数通过递减满堆栈传递
c、函数返回的整型变量通过寄存器r0传递
函数参数传递图:
…
sp+16 参数8
sp+12 参数7
sp+8 参数6
sp+4 参数5
sp 参数4
r3 参数3
r2 参数2
r1 参数1
r0 参数0 返回值
r0-r3:用来存放函数的前4个参数,r0还用于保存函数的返回值
r4-r12: 可以用来分配局部变量,但使用前要通过堆栈保存
r13:栈指针
r14:函数调用时保存返回地址
r15:pc
函数调用的优化原则:尽量限制一个函数的参数,不要超过4个。
F、常用指令
1、数据处理指令:mov/mvn
最简单的ARM指令,执行的结果就是把一个立即数N送到目标寄存器Rd,N可以是通用寄存器,也可以是常量。语法:
mov{<cond>}{S} Rd, N mvn{<cond>}{S} Rd, ~N
mov:把一个32位数存入寄存器Rd,即Rd = N
mvn:把一个32位数按位取反后存入寄存器Rd
N为立即数,可以是一个寄存器Rn,也可以是一个使用”#”作为前缀的常量。指令可支持条件执行,可以附加S位。
mov r7, r5 mov r4, #0xbd00
2、算术指令:add/sub
用于实现32位有符号数或无符号数的加法和减法。语法:
add{<cond>}{S} Rd, Rn, N sub{<cond>}{S} Rd, Rn, N
add:32位加法,Rd=Rn+N
sub:32位减法,Rd=Rn-N
N为立即数,可以是一个寄存器Rm,也可以是一个使用”#”作为前缀的常量,还可以包括移位
sub r0, r1, r2 add r0, r1, r1
3、逻辑指令
对2个源操作数进行按位的逻辑操作,结果存储到目标寄存器中。语法:
and{<cond>}{S} Rd, Rn, N orr{<cond>}{S} Rd, Rn, N eor{<cond>}{S} Rd, Rn, N bic{<cond>}{S} Rd, Rn, N
and:32位逻辑与;Rd=Rn & N
orr:32位逻辑或;Rd=Rn | N
eor:32位逻辑异或;Rd=Rn ^ N
bic:32位逻辑位清除;Rd=Rn &~ N
N可以是寄存器,也可以是常量,还可以包括移位。
and r0, r1, r2 orr r0, r1, r2 eor r0, r1, r2 bic r0, r1, r2
bic指令通常用于清除寄存器的某个特定位,比如清除CPSR寄存器的I位和F位来使能中断;eor指令常用于反转某些位。
4、比较指令
比较指令用于对一个寄存器中的值和一个立即数进行比较或测试。比较指令根据结果更新cpsr的标志位,但不影响其它的寄存器。当比较指令改变了标志位后,后续指令就可以通过条件执行来改变程序的执行流程。比较指令不使用S后缀就可以改变标志位。语法:
cmp{<cond>} Rn, N teq{<cond>} Rn, N tst{<cond>} Rn, N
cmp:比较(根据Rn-N设置标记位)
teq:等值比较(根据Rn^N设置标记位)
tst:位测试(根据Rn&N设置标记位)
cmp r0, r9
5、分支指令(branch)
分支指令可以改变程序的执行流程或者调用子程序。这种指令使得程序可以实现子程序调用,if-else结构以及循环等;执行流程的改变会迫使程序计数器pc指向一个新的地址。语法:
b{<cond>} label bl{<cond>} label
b:跳转,pc=label
bl:带返回的跳转,pc=label,lr=bl指令后面一条指令的地址
b指令一般用于实现C代码中的if-else分支,或者for/while循环;bl指令一般用于实现函数调用/返回。
myadd: add r0, r1, r2 ... b myadd ...
标号放在一行的开始处,后面加冒号。
@函数定义 myfunc: ... mov pc, lr @返回 @当r0等于0时调用函数myfunc cmp r0, #0 bleq myfunc ...
带条件的跳转bleq
1: ... 1: ... b 1b @backward/forward ... 1: ...
在一个.s文件中,字母开头的标号视为全局标号,是不能重名的;而纯数字的标号认为是局部标号,可以重名。
6、单寄存器load-store指令
load-store指令用于在内存和通用寄存器之间交换数据;ARM是不能直接操作内存中的数据的,必须首先把数据用load指令从内存读取到通用寄存器中,然后才能进行计算,最后再用store指令将运算的结果写回内存。语法:
ldr{<cond>} Rd, address str{<cond>} Rd, address
ldr:把一个word(32位)从内存读入通用寄存器Rd
str:把一个通用寄存器的内容(32位)写入内存
如果需要读写16位数据,可以用ldrh/strh
如果需要读写8位数据,可以用ldrb/strb
address是内存中的地址,可以直接写一个标号;也可以先把基地址存到寄存器中,然后采用[基地址+/-偏移量]的方式访问内存。偏移量也可以有多种表示,可以直接用常数作为偏移量,也可以用另一个寄存器做偏移量。
[Rn, #+/-offset_12] [Rn, +/-Rm] [Rn, +/-Rm, shift_imm]
ldr和str需要装载和存储地址对齐的数据,如ldr只能从0、4、8等地址装载32位的word。
例1:
@在r1中保存基地址 @将内存中的数据读入r0 @不能写成ldr r0, [#0x8000] mov r1, #0x8000 ldr r0, [r1, #0x20]
例2:
@将r0中的数据写入内存 mov r1, #0x9000 str r0, [r1] str r0, [r1, #-8]
ldr/str指令的寻址方式:
基地址寻址:
mov r1, #0x8000 ldr r0, [r1] @从地址单元0x8000读4个字节,存入r0 @r1的内容不变
基地址加偏移寻址:
mov r1, #0x8000 ldr r0, [r1, #8] mov r2, #0x100 ldr r3, [r1, r2] @从地址单元0x8008读4个字节,存入r0 @从地址单元0x8100读4个字节,存入r3 @r1的内容不变
基地址加偏移并更新基地址:
mov r1, #0x8000 ldr r0, [r1, #8]! ldr r2, [r1, #4] @从地址单元0x8008读4个字节,存入r0 @r1的内容变为0x8008 @从地址单元0x800C读4个字节,存入r2
注意:!的作用是在指令执行完毕后,更新基地址;
只更新基地址:
mov r1, #0x8000 ldr r0, [r1], #8 ldr r2, [r1] @从地址单元0x8000读4个字节,存入r0 @r1的内容变为0x8008 @从地址单元0x8008读4个字节,存入r2
基于标号寻址:
在汇编中,标号实际上也是地址,因此可以直接在ldr/str指令后面使用标号。
abc: .word 0x12345678 ... ldr r0, abc @从地址单元abc读4个字节,存入r0
7、多寄存器传输指令ldm/stm
为了提高效率,ARM还提供了和内存单元交换多个word的指令,这种指令常用于数据拷贝,入栈出栈等操作;多寄存器传输指令是执行时间最长的ARM指令。语法:
ldm{<cond>}<寻址模式> Rn{!}, <Regs>{^} stm{<cond>}<寻址模式> Rn{!}, <Regs>{^}
ldm:从内存中取出数据装载到多个寄存器中
stm:将多个寄存器中的内容保存到内存中
Rn中保存要访问的内存单元的基地址
!的作用是在指令执行完毕后,更新基地址
Regs是指令涉及的通用寄存器列表
^可以将spsr寄存器的值赋给cpsr
ldm/stm指令的寻址模式:IA/IB/DA/DB
IA(执行后增加,Increase After)
mov r7, #0x8000 ldmia r7!, {r0-r2,r5} @从0x8000开始连续读16个字节(地址递增) @0x8000~0x8003的内容存入r0 @0x8004~0x8007的内容存入r1 @0x8008~0x800b的内容存入r2 @0x800c~0x800f的内容存入r5 @将r7的内容更新为0x8010
IB(执行前增加,Increase Before)
mov r7, #0x8000 ldmib r7!, {r5,r0-r2} @从0x8004开始连续读16个字节(地址递增) @按顺序存入r0,r1,r2,r5 @将r7的内容更新为0x8010
DA(执行后减少,Decrease After)
mov r7, #0x8000 ldmda r7!, {r0-r2,r5} @从0x8000开始连续读16个字节(地址递减) @0x8000~0x8003的内容读入r5 @0x7ffc~0x7fff的内容读入r2 @0x7ff8~0x7ffb的内容读入r1 @0x7ff4~0x7ff7的内容读入r0 @将r7的内容更新为0x7ff0
DB(执行前减少,Decrease Before)
mov r7, #0x8000 ldmdb r7!, {r0-r2,r5} @从0x8000开始连续读16个字节(地址递减) @0x7ffc~0x7fff的内容读入r5 @0x7ff8~0x7ffb的内容读入r2 @0x7ff4~0x7ff7的内容读入r1 @0x7ff0~0x7ff3的内容读入r0 @将r7的内容更新为0x7ff0
stm/ldm指令常用于栈的操作以及数据拷贝:
入栈和出栈:
@设置栈顶为0x8000 mov sp, #0x8000 stmib sp!, {r1-r3} ... ldmda sp!, {r1-r3}
C代码默认使用stmdb和ldmia的组合
数据拷贝:
@r9存放数据的源地址(0x8000) @r10存放数据大小(0x100) @r11存放目的地址(0xA000) mov r9, #0x8000 mov r10, #0x100 mov r11, #0xA000 loop: ldmia r9!, {r0-r7} stmia r11!, {r0-r7} sub r10, r10, #32 cmp r10, #0 bgt loop
8、软件中断指令swi/svc
中断是由硬件产生的异常,而软件中断是通过执行特定的指令产生的异常。ARM定义了指令swi(ARMv7后更名为svc)来产生一个软件中断异常;当处理器执行完swi指令后,不会继续执行swi后面的指令,而是首先将CPU切换到svc模式,然后将pc设定为异常向量表基地址+0x8,并从此处继续执行。swi/svc指令的最主要工作就是将cpu从非特权的usr模式(对应linux的用户态)提升到特权的svc模式(对应Linux的内核态)。在Linux用户态执行的open/read等系统调用,在ARM平台上就是通过swi指令实现的。通常在用户模式下调用swi,从而使处理器从非特权模式(usr)切换到特权模式(svc)。
语法:
swi{<cond>} number
指令后面的数字为系统调用号,最大为0xffffff。系统调用号直接编译到指令代码中。
执行swi指令后,处理器的执行步骤如下:
–> 在lr_svc寄存器中记录swi指令后面一条指令的地址
–> 将cpsr的值(执行swi指令时所处的处理器状态)存入spsr_svc
–> pc = 异常向量表基地址 + 0x8
–> 将cpu切换到svc模式
–> cpsr_I = 1(屏蔽IRQ中断)
执行编号为123的系统调用:
swi 123 @执行前,cpsr=nzcVift_USER @执行前,pc=0x8000 @执行后,cpsr=nzcVift_SVC @执行后,spsr=nzcVift_USER @执行后,pc=0x08, lr=0x8004 @可以用r0传递参数
软件中断的处理函数,应该从异常向量表的0x8单元处跳转过来:
swi_handler: stmdb sp!, {r0-r12,lr} ldr r0, [lr, #-4] bic r0, r0, #0xff000000 bl swi_service ldmia sp!, {r0-r12, pc}^ @将r0-r12,lr压栈 @读swi指令的编码,放入r0 @取出swi指令的后24位(即系统调用号) @根据系统调用号调用相应的处理程序 @出栈,并跳转回swi后面的指令
在重新装载pc的时候,除非明确要求,否则不会把spsr中的内容恢复到cpsr中去。要恢复cpsr的方法是在寄存器列表后跟随一个”^”。
9、状态寄存器访问指令mrs/msr
要读写ARM的状态寄存器cpsr和spsr,需要使用两条特殊的指令:mrs和msr。语法:
mrs{<cond>} Rd, <cpsr|spsr> msr{<cond>} <cpsr|spsr>_<field>, Rm | #立即数
mrs:将状态寄存器的内容读入通用寄存器Rd
msr:设定状态寄存器或其中的某个域
field:用msr指令写cpsr/spsr时,可以附加field标志从而只影响cpsr/spsr的部分位
支持的field标志有:
c – control域(bit[7:0])
x – extension域(bit[15:8])
s – status域(bit[23:16])
f – flags域(bit[31:24])
使能IRQ:
mrs r1, cpsr bic r1, r1, #0x80 msr cpsr_c, r1 @执行前,cpsr=nzcvIFt_SVC @执行后,cpsr=nzcviFt_SVC
10、协处理器访问指令
ARM提供了专门的协处理器访问指令,协处理器可提供附加的计算能力,如浮点数运算、多媒体数据运算等,也可用于控制cache和MMU等,协处理器访问指令的格式必须参考对应的ARM手册。语法:
mrc{<cond>} cp, opcode1, Rd, Cn, Cm{, opcode2} mcr{<cond>} cp, opcode1, Rd, Cn, Cm{, opcode2}
mrc:把数据从协处理器内部的寄存器读入ARM的通用寄存器
mcr:把通用寄存器中的数据写入协处理器内部的寄存器
cp:协处理器的编号,范围p0~p15
Rd:ARM中的通用寄存器,范围r0~r12
opcode1:操作码1,范围0~7
opcode2:操作码2,范围0~7
Cn:协处理器中的主寄存器,范围c0~c15
Cm:协处理器中的辅助寄存器,范围c0~c15
注意!op1, op2, Cn, Cm组合在一起代表特定的操作,其组合方式只能参考对应的手册。
11、ARM的伪指令
为了满足编程的需要,ARM还提供了一些伪指令。伪指令的使用方式和前面的指令一样,但汇编器(as)在汇编时,会首先根据具体情况把伪指令转换为前面介绍过的真正的ARM指令,然后再进行编码。
(1)压栈和出栈:
为了更好地使用堆栈,ARM根据栈的不同使用状态,提供了专门的入栈/出栈指令。这些指令会被汇编器转换为前面介绍的stm/ldm系列指令。
堆栈的分类:
从栈指针的生长方向上来划分,栈可以分为递增堆栈和递减堆栈
从栈指针指向的单元是否被使用上来划分,栈可以分为满堆栈和空堆栈
根据这两种分类,栈的使用可分为4种:
A. 递增满堆栈(FA, full ascend)
B. 递减满堆栈(FD, full descend)
C. 递增空堆栈(EA, empty ascend)
D. 递减空堆栈(ED, empty descend)
在Uboot或Linux中,默认采用递减满的方式使用堆栈。
递减满堆栈:
mov sp, #0x8000 stmfd sp!, {r0-r2} ... ldmfd sp!, {r0-r2} @栈指针sp指向堆栈的顶部0x8000 @压栈后,0x7ffc存r0, 0x7ff8存r1, 0x7ff4存r2 @sp更新到0x7ff4(指向的单元中包含有效数据) @出栈后,sp重新指向栈顶0x8000 @stmfd = stmdb, ldmfd = ldmia
(2)常量和地址装载:
ARM指令采用32位编码,由于操作码要占用空间,因此不可能在一条指令中编码一个32位的立即数,为方便编程,ARM增加了一条伪指令,用于把一个32位的立即数存入寄存器。ARM还提供了一条伪指令,用于获得当前的运行地址。
语法:
ldr Rd, =constant adr Rd, label
ldr:常量装载伪指令,Rd=32位常量
adr:地址装载伪指令,Rd=32位相对地址
例1:
ldr r0, =0x12345678 @伪指令由编译器展开为ARM指令,反汇编一下看看
例2:
abc: adr r0, abc @将标号abc在内存中的实际地址存储到r0中
例3:
abc: ldr r0, =abc ldr r1, abc @将链接时指定的标号abc的地址值赋给r0 @单寄存器ldr指令,将标号abc所指地址的内存的一个word读入r1寄存器
12、gcc编译器支持的伪汇编指令
在ARM汇编语言程序设计中,有一些特殊指令助记符,这些助记符与ARM指令不同,没有相对应的操作码,通常称这些特殊指令助记符为伪汇编指令或伪指令。伪指令类似于C语言中的include,define等,仅在程序的汇编过程中起作用,一旦汇编结束,伪指令的使命就完成。
伪汇编是编译器定义的,如果不是使用gcc,而是armcc,则编译器支持不同格式的伪汇编。
常用的伪汇编指令:
(1)标号(label)
gcc中的标号通过后面带的冒号来识别,例如:
.global add add: add r0, r0, r1 mov pc, lr
(2).global/.globl
将symbol定义为全局标号,例如:
.global MyAsmFunc
(3).byte
声明一个字节(8bit),相当于c语言中的char,例如:
hello: .byte 25, 0x11, 'A'
(4).hword/.short
声明一个半字(16bit),相当于c语言中的short,例如:
hello: .hword 2, 0xFFE0
(5).word/.int/.long
声明一个字(32bit),相当于C语言中的int,例如:
hello: .word 144511, 0x223344 .word 0x11
(6).ascii
声明一个字符串(非零结束符),例如:
hello: .ascii "Ascii text is here"
(7).asciz/.string
声明一个字符串(以0为结束符),例如:
.asciz "Zero Terminated Text" .string "My Cool String\n"
(8).fill
语法:
.fill repeat {, size} {, value}
数据填充,次数为repeat次,每次填充size个字节,填充内容为value,size缺省为1, value缺省为0。例如:
.fill 32, 4, 0xFFFFFFFF
(9).space
语法:
.space size {, value}
用value填充size个字节,value缺省为0。例如:
.space 25, 0b11001100
(10).arm/.code 32
定义以下代码使用ARM指令集编译。
.arm
(11).include
包含文件,可以包含头文件或其他汇编文件
.include "hardware.bin"
更常见的头文件包含方式为:
#include <linux/config.h>
(12).align/.balign
语法:
.balign {alignment} {, fill} {, max}
以alignment个字节对齐。
(13).end
标记汇编文件的结束行,即标号后的代码不作处理。
.end
(14).section
定义elf文件中包含的段,可以是.text、.data、.bss等。
.section .bss
(15).text/.data/.bss
将符号后面的代码或数据编译到指定段中。
.text .data .bss
(16).if/.elseif/.else/.endif
条件编译,例如:
.if (2+2) ... .endif
(17).ifdef
若symbol已定义,编译以下代码:
.ifdef _test_i_
(18).ifndef
若symbol没定义,编译以下代码:
.ifndef _test_i_
(19).rept/.endr
重复编译.rept与.endr之间的语句count次,例如:
.rept 8 mov r0, r0 .endr
(20).equ/.equiv
将符号替换为表达式的结果,类似于c中的define,例如:
.equ swapper_pg_dir, KERNEL_RAM_ADDR - 0x4000 .equiv Version, "0.2"
(21).req
为ARM的通用寄存器起个名字,语法:
<register_name> .req <register_name>
例如:
acc .req r0