1. .align
.align的作用在于对指令或者数据的存放地址进行对齐,有些CPU架构要求固定的指令长度并且存放地址相对于2的幂指数圆整,否则程序无法正常运行,比如ARM;有些系统却不需要,如果不遵循地址的圆整规则,程序依然可以正确执行,只是降低了一些执行效率,比如i386。.align的作用范围只限于紧跟它的那条指令或者数据,而接下来的指令或者数据的地址由上一条指令的地址和其长度决定。
ARM汇编器并不直接使用.align提供的参数作为对齐目标,而是使用2^n的值,比如这里的参数为4,那么圆整对象为2^4 = 16。这也就是为什么在ARM平台的Uboot或者Linux内核汇编中会出现.align 5的根本原因。.align此时的取值范围为0-15,当取值为0,2或者不提供参数时均圆整于4。如果尝试使用大于15的值,将会得到编译器的err。
在指令出现非对齐情况下,插入.align伪指令,对于32bit的ARM会进行4byte的指令对齐。
2. .rept
.rept和.endr之间的语句count次。
3. .text
几个常用的段代号,基本上与编译器/处理器都没有无关系(FLAT模式):
.text - 代码段
.const - 只读数据段(有些编译器不使用此段,将只读数据并入.data段)
.data - 读写数据段
.bss - 堆
4. .extern
".extern"定义一个外部符号(可以是变量也可以是函数。
5. .global
".global"将本文件中的某个程序标号定义为全局的。
6. .word
.word expression就是在当前位置放一个word型的值,这个值就是expression。
相当于用.word定义了一个16bit的数据。
举例来说,
_rWTCON:
.word 0x15300000
就是在当前地址,即_rWTCON处放一个值0x15300000
7. 更多伪指令
http://www.byywee.com/page/M0/S774/774183.html
8. 条件码表
条件码助记符 |
标志 |
含义 |
EQ |
Z=1 |
相等 |
NE |
Z=0 |
不相等 |
CS/HS |
C=1 |
无符号数大于或等于 |
CC/LO |
C=0 |
无符号数小于 |
MI |
N=1 |
负数 |
PL |
N=0 |
正数 |
VS |
V=1 |
溢出 |
VC |
V=0 |
没有溢出 |
HI |
C=1,Z=0 |
无符号数大于 |
LS |
C=0,Z=1 |
无符号数小于或等于 |
GE |
N=V |
带符号数大于或等于 |
LT |
N!=V |
带符号数小于 |
GT |
Z=0,N=V |
带符号数大于 |
LE |
Z=1,N!=V |
带符号数小于或等于 |
AL |
|
任何无条件执行(指令默认条件) |
9. ldr
伪指令LDR
大范围的地址读取伪指令.LDR 伪指令用于加载32位的立即数或一个地址值到指定寄存器.在汇编编译源程序时,LDR伪指令被编译器替换成一条合适的指令.若加载的常数未超出MOV或MVN的范围,则使用MOV或MVN指令代替该LDR伪指令,否则汇编器将常量放入字池,并使用一条程序相对偏移的LDR指令从文字池读出常量.LDR伪指令格式如下:
LDR{cond} register,=expr/label_expr
其中 register 加载的目标寄存器
expr 32 位立即数.
label_expr 基于PC 的地址表达式或外部表达式.
LDR/STR 指令用于对内存变量的访问,内存缓冲区数据的访问、查表、外围部件的控制操作等等,若使用LDR指令加载数据到PC寄存器,则实现程序跳转功能,这样也就实现了程序散转。
ldr r1, [r2, #4] /*将地址为r2+4的内存单元数据读取到r1中*/
ldr r1,[r2] /*将地址为r2的内存单元数据读取到r1中*/
ldr r1,[r2], #4/*将地址为r2的内存单元数据读取到r1中,然后r2=r2+4*/
str r1 ,[r2, #4]/*将r1的数据保存到地址为r2+4的内存单元中*/
str r1, [r2]/*将r1的数据保存到地址为r2的内存单元中。*/
str r1, [r2],#4/*将r1的数据保存到地址为r2的内存单元,然后r2= r2+4*/
ldrb:8bit=>1byte
ldrh:16bit=>2byte
LDR R0,LED_TAB
LDR R1, =LED_TAB
LED_TAB: .work 0x12345678
R0的值是0x12345678,R1的值是LED_TAB标号值,就是0x12345678在内存中存放的地址
10. adr
转自:http://coon.blogbus.com/logs/2738861.html
ldr r0, _start
adr r0, _start
ldr r0, =_start
nop
mov pc, lr
_start:
nop
编译的时候设置 R0 为 0x0c008000
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
0c008000 <_start-0x14>:
c008000: e59f000c ldr r0, [pc, #12] ; c008014 <_start>
c008004: e28f0008 add r0, pc, #8 ; 0x8
c008008: e59f0008 ldr r0, [pc, #8] ; c008018 <_start+0x4>
c00800c: e1a00000 nop (mov r0,r0)
c008010: e1a0f00e mov pc, lr
0c008014 <_start>:
c008014: e1a00000 nop (mov r0,r0)
c008018: 0c008014 stceq 0, cr8, [r0], -#80
分析:
ldr r0, _start
从内存地址 _start 的地方把值读入。执行这个后,r0 = 0xe1a00000
adr r0, _start
取得 _start 的地址到 r0,但是请看反编译的结果,它是与位置无关的。其实取得的时相对的位置。例如这段代码在 0x0c008000 运行,那么 adr r0, _start 得到 r0 = 0x0c008014;如果在地址 0 运行,就是 0x00000014 了。
ldr r0, =_start
这个取得标号 _start 的绝对地址。这个绝对地址是在 link 的时候确定的。看上去这只是一个指令,但是它要占用 2 个 32bit 的空间,一条是指令,另一条是 _start 的数据(因为在编译的时候不能确定 _start 的值,而且也不能用 mov 指令来给 r0 赋一个 32bit 的常量,所以需要多出一个空间存放 _start 的真正数据,在这里就是 0x0c008014)。
因此可以看出,这个是绝对的寻址,不管这段代码在什么地方运行,它的结果都是 r0 = 0x0c008014
11. ldm
ldm和stm属于批量内存访问指令,只用一条指令就可以读写多个数据。它们的格式如下:
ldm{cond}<addressing_mode> <rn>{!} <register list>{^}
stm{cond}<addressing_mode> <rn>{!} <register list>{^}
其中,
{cond}表示指令的执行条件,参见下面的指令条件码。
<addressing_mode>表示地址变化模式,有以下几种方式:
1)ia(increment after): 传送后递增方式;
2)ib(increment before): 传送前递增方式;
3)da(decrement after): 传送后递减方式;
4)db(decrement before):传送前递减方式;
5)fd(full descending): 满递减堆栈;
6)ed(empty descending):空递减堆栈;
7)fa(full ascending): 满递增堆栈;
8)ea(empty ascending): 空递增堆栈;
<rn>中保存内存的地址,如果后面加上了感叹号,指令执行后,rm的值会更新,等于下一个内存单元的地址。
<register list>表示寄存器列表,对于ldm指令,从<rn>所对应的内存块中取出数据,写入这些寄存器;对于stm指令,把这些寄存器的值写入<rn>所对应的内存块中。
{^}有两种含义:如果<register list>中有PC寄存器,它表示指令执行后,spsr寄存器的值将自动复制到cpsr寄存器中——这常用于从中断处理函数中返回;如果<register list>中没有PC寄存器,{^}表示操作的是用户模式下的寄存器,而不是当前特权模式的寄存器。
指令中寄存器列表和内存单元的对应关系为:编号低的寄存器对应于内存中的低地址单元,编号高的寄存器对应于内存中高地址的单元。
ldmia r0!, {r3-r10} /*将基址寄存器r0开始的连续8个地址单元的值分别赋给r3,r4,r5,r6,r7,r8,r9,r10,注意的是r0指定的地址每次赋一次r0会加1,指向下一个地址单元*/
stmia r1!, {r3-r10} /*跟上面指令功能相反,将寄存器r3到r10的值依次赋值给r1指定的地址单元,每次赋值一次r1就加1*/
堆栈寻址:堆栈是特定顺序进行存取的存储区,堆栈寻址时隐含的使用一个专门的寄存器(堆栈指针),指向一块存储区域(堆栈),存储器堆栈可分为两种:
向上生长:向高地址方向生长,称为递增堆栈。
向下生长:向低地址方向生长,称为递减堆栈。
如此可结合出四种情况:
1、满递增:堆栈通过增大存储器的地址向上增长,堆栈指针指向内含有效数据项的最高地址,栈指针总是指向最后一个元素(最后入栈的数据),指令如 LDMFA,STMFA。
2、空递增:堆栈通过增大存储器的地址向上增长,堆栈指针指向堆栈上的第一个空位置,栈指针总是指向下一个将要放入数据的空位置,指令如 LDMEA,STMEA。
3、满递减:堆栈通过减小存储器的地址向下增长,堆栈指针指向内含有效数据项的最低地址,栈指针总是指向最后一个元素(最后入栈的数据),指令如 LDMFD,STMFD。
4、空递减:堆栈通过减小存储器的地址向下增长,堆栈指针指向堆栈下的第一个空位置,栈指针总是指向下一个将要放入数据的空位置,指令如 LDMED,STMED。
满:栈指针总是指向最后一个元素(最后入栈的数据)。
空:栈指针总是指向下一个将要放入数据的空位置。
增:栈首部是低地址,栈向高地址增长。
减:栈首部是高地址,栈向低地址增长。
STMFD SP!,{R1-R7,LR} ;将R1-R7,LR入栈,满递减堆栈
LDMFD SP!,{R1-R7,LR} ;数据出栈,放入R1-R7,LR寄存器,满递减堆栈
ARM-Thumb过程调用标准和ARM、Thumb C/C++ 编译器总是使用Full descending 类型堆栈。
以前困惑的就是STMFD 命令 对于操作数 是按照什么顺序压栈的
比如:STMFD sp!{R0-R5,LR} 进栈顺序是:
高地址(1方式) LR R5 R4 ``````` R0 <-sp 低地址
高地址(2方式) R0 R1 ``` R5 LR <-sp 低地址
寻址方式 |
说明 |
pop |
=LDM |
push |
=STM |
FA |
递增满 |
LDMFA |
LDMDA |
STMFA |
STMIB |
FD |
递减满 |
LDMFD |
LDMIA |
STMFD |
STMDB |
EA |
递增空 |
LDMEA |
LDMDB |
STMEA |
STMIA |
ED |
递减空 |
LDMED |
LDMIB |
STMED |
STMDA |
按照图表,可知 STMFD对应的是STMDB,根据arm指令手册,可知STMDB入栈顺序是1方式,而LDMFD对应的是LDMIA,这样这两个操作就可以成功配对。
在寄存器传输中,基地址可以在传输前或者传输后递增或者递减:STMIA r10,{r1,r3-r5,r8}
http://blog.csdn.net/xiaomt_rush/article/details/6501711
12. ldrex
在 include/asm-arm/spinlock.h 下有這麼一段
#if __LINUX_ARM_ARCH__ < 6
#error SMP not supported on pre-ARMv6 CPUs
#endif
好啦,前提就是:只有 ARM core版本 >=6才可以繼續:
all spin lock primitives 到最後都是使用下面這個基本型:
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
unsigned long tmp;
1 __asm__ __volatile__(
2"1: ldrex %0, [%1]\n"
3" teq %0, #0\n"
4" strexeq %0, %2, [%1]\n"
5" teqeq %0, #0\n"
6" bne 1b"
7 : "=&r" (tmp)
8 : "r" (&lock->lock), "r" (1)
9 : "cc");
smp_mb();
}
[指令重點]:
ldrex 指令是 core 6 以後才有的,跟 strex 配成一對指令,可以請 bus 監控從 ldrex 到 strex 之間有無其他的 CPU 或 DMA 來存取這個位址 ,若有的話,strex 會在第一個 register 裡設定值為 1(non-exclusive by this CPU) 並且令 store 動作失敗,若沒有,strex 會在第一個 register 裡設定值為 0(exclusive access by this CPU) 並且令 store 動作成功。
Code Trace Discussion:
Line 1: __volatile__ 告訴 compiler ,不要對這塊 assembly template 做最佳化動作,因為我們裡面有 loop 讀取 memory 動作,最佳化的結果可能導致 compiler 用一個 register 來 cache 它的值,不會老老實實的去讀 memory... 呵呵,這不是我們想要的動作喔!
Line 2: 把 lock 讀到 tmp,並請 bus monitor 這個 memory。
Line 3: 測試 lock 是否為 0,若非 0,表示 lock 已經被別人取得了,則 Line 4,5 都不做了,然後 Line 6 一定 branch,做 spin 的動作。若為 0,表示有機會取得 lock,繼續做 Line 4.5.。
Line 4: 重點來了!,核對 bus monitor 的結果,若是exclusive access 則 tmp 設為 0 並且把 1 儲存到 lock,若是 non-exclusive access(有其他 CPU 來動過)則 tmp設為 1並且不做儲存 lock 的動作。
Line 5: 測試 tmp。
Line 6: 若 tmp 為 0 表示剛剛對 lock 動作是 exclusive,可以離開迴圈,若 tmp 為 1,則做 spin 動作。
Line 7: tmp 用 register 來操作,同時是 input 及 output 令它為 %0。
Line 8: &lock->lock 用 register 來操作 ,令它為 %1,值 1 用 register 來操作 ,令它為 %2。
Line 9: 此 template 會改到 condition code,加入 clobber list 以告訴 compiler 有這回事。
好了,終於看完了,真的很佩服那些 coding 的 kernel hackers ....
思考問題: ARM v6 以前有個 SWP 指令可以 lock bus and swap memory ,一樣可以用來完成 exclusive access ,但是比起 ldrex,strex 這對指令有什麼缺點呢?
SWP lock bus,其他 CPU 所有動作都不能做,但是 ldrex,strex就不會有這種現象,使用 ldrex,strex 時若其他 CPU不來 access 這個特定的 memory 就可以平行的做動作,增加平行執行的 performance。
13. swi
SWI,即software interrupt软件中断。该指令产生一个SWI异常。意思就是处理器模式改变为超级用户模式,CPSR寄存器保存到超级用户模式下的SPSR寄存器,并且跳转到SWI向量。其ARM指令格式如下:
SWI{cond} immed_24
Cond域:是可选的条件码 (参见 ARM汇编指令条件执行详解).
immed_24域:范围从 0 到 224-1 的表达式, (即0-16777215)。用户程序可以使用该常数来进入不同的处理流程。
一、方法1:获取immed_24操作数。
为了能实现根据指令中immed_24操作数的不同,跳转到不同的处理程序,所以我们往往需要在SWI异常处理子程序中去获得immed_24操作数的实际内容。获得该操作数内容的方法是在异常处理函数中使用下面指令:
LDR R0,[LR,#-4]
该指令将链接寄存器LR的内容减去4后所获得的值作为一个地址,然后把该地址的内容装载进R0。此时再使用下面指令,immed_24操作数的内容就保存到了R0:
BIC R0,R0,#0xFF000000
该指令将R0的高8位清零,并把结果保存到R0,意思就是取R0的低24位。
可能还是有人会问:为什么在SWI异常处理子程序中执行这两条指令后,immed_24操作数的内容就保存到了R0寄存器呢?之所以会有这样的疑问,基本都是因为对LR寄存器的作用没了解清楚。下面介绍一下链接寄存器LR(R14)的作用。
寄存器R14(LR寄存器)有两种特殊功能:
·在任何一种处理器模式下,该模式对应的R14寄存器用来保存子程序的返回地址。当执行BL或BLX指令进行子程序调用时,子程序的返回地址被放置在R14中。这样,只要把R14内容拷贝到PC中,就实现了子程序的返回(具体的子程序返回操作,这里不作详细介绍)。
·当某异常发生时,相应异常模式下的R14被设置成异常返回的地址(对于某些异常,可能是一个偏移量,一个较小的常量)。异常返回类似于子程序返回,但有小小的不同(这里不作详细介绍)。
所谓的子程序的返回地址,实际就是调用指令的下一条指令的地址,也就是BL或BLX指令的下一条指令的地址。所谓的异常的返回的地址,就是异常发生前,CPU执行的最后一条指令的下一条指令的地址。
例如:(子程序返回地址示例)
指令 指令所在地址
ADD R2,R1,R3 ;0x300000
BL subC ;0x300004
MOV R1,#2 ;0x300008
BL指令执行后,R14中保存的子程序subC的返回地址是0x300008。
再例如:(异常返回地址示例)
指令 指令所在地址
ADD R2,R1,R3 ;0x300000
SWI 0x98 ;0x300004
MOV R1,#2 ;0x300008
SWI指令执行后,进入SWI异常处理程序,此时R14中保存的返回地址为0x300008。
所以,在SWI异常处理子程序中执行 LDR R0,[LR,#-4]语句,实际就是把产生本次SWI异常的SWI指令的内容(如:SWI 0x98)装进R0寄存器。又因为SWI指令的低24位保存了指令的操作数(如:0x98),所以再执行BIC R0,R0,#0xFF000000语句,就可以获得immed_24操作数的实际内容。
二、方法2:使用参数寄存器。
实际上,在SWI异常处理子程序的实现时,还可以绕开immed_24操作数的获取操作,这就是说,我们可以不去获取immed_24操作数的实际内容,也能实现SWI异常的分支处理。这就需要使用R0-R4寄存器,其中R0-R4可任意选择其中一个,一般选择R0,遵从ATPCS原则。
具体方法就是,在执行SWI指令之前,给R0赋予某个数值,然后在SWI异常处理子程序中根据R0值实现不同的分支处理。例如:
指令 指令所在地址
MOV R0,#1 ; #1给R0
SWI 0x98 ; 产生SWI中断,执行异常处理程序SoftwareInterrupt
ADD R2,R1,R3 ;
;SWI异常处理子程序如下
SoftwareInterrupt
CMP R0, #6 ; if R0 < 6
LDRLO PC, [PC, R0, LSL #2] ; if R0 < 6,PC = PC + R0*4,else next //PC-8处的指令
MOVS PC, LR //PC-4处的指令
SwiFunction
DCD function0 ;0//PC处的指令
DCD function1 ;1
DCD function2 ;2
DCD function3 ;3
DCD function4 ;4
DCD function5 ;5
Function0
异常处理分支0代码
Function1
异常处理分支1代码
function2
异常处理分支2代码
function3
异常处理分支3代码
function4
异常处理分支4代码
function5
异常处理分支5代码
在ARM体系结构中,当正确读取了PC的值时,该值为当前指令地址值加8字节,也就是说,对于ARM指令集来说,读出的PC值指向当前指令的下两条指令的地址,本例中就是指向SwiFunction 表头DCD function0这个地址,在该地址中保存了异常处理子分支function0的入口地址。所以,当进入SWI异常处理子程序SoftwareInterrupt时,如果R0=0,执行LDRLO PC, [PC, R0, LSL #2]语句后,PC的内容即为function0的入口地址,即程序跳转到了function0执行。在本例中,因为R0=1,所以,实际程序是跳转到了function1执行。R0左移2位(LDRLO PC, [PC,R0, LSL #2]),即R0*4,是因为ARM指令是字(4个字节)对齐的DCD function0等伪指令也是按4字节对齐的。
在本方法的实现中,实际指令中的24位立即数(immed_24域)被忽略了, 就是说immed_24域可以为任意合法的值。如在本例中,不一定使用SWI 0x98,还可以为SWI 0x00或者SWI 0x01等等,程序还是会进入SWI异常处理子程序SoftwareInterrupt,然后根据R0的内容跳转到相应的子分支。
ARM处理器使用流水线来增加处理器指令流的速度,这样可使几个操作同时进行,并使处理与存储器系统之间的操作更加流畅,连续,能提供0.9MIPS/MHZ的指令执行速度。 PC代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:
1.取指(从存储器装载一条指令);
2.译码(识别将要被执行的指令);
3.执行(处理指令并将结果写回寄存器)。
而R15(PC)总是指向“正在取指”的指令,而不是指向“正在执行”的指令或正在“译码”的指令。一般来说,人们习惯性约定将“正在执行的指令作为参考点”,称之为当前第一条指令,因此PC总是指向第三条指令。当ARM状态时,每条指令为4字节长,所以PC始终指向该指令地址加8字节的地址,即:PC值=当前程序执行位置+8;
周期1 周期2 周期3 周期4 周期5 周期6
PC-8 取指 译码 执行
PC-4 取指 译码 执行
PC 取指 译码 执行
14. cmp
CMP比较指令,用于把一个寄存器的内容和另一个寄存器的内容或一个立即数进行比较,同时更新CPSR中条件标志位的值。指令将第一操作数减去第二操作数,但不存储结果,只更改条件标志位。
CMP R1, R0 ;做R1-R0的操作。
CMP R1,#10 ;做R1-10的操作。
15. txt
TST位测试指令,用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的与运算,并根据运算结果更新CPSR中条件标志位的值。操作数1是要测试的数,而操作数2 是一个位掩码,该指令一般用来检测是否设置了特定的位。
TST {条件} 操作数1, 操作数2
例:TST R0, #0X0000 0040 ; 指令用来测试R0的位3是否为1。
TST指令通常和EQ、NE条件码配合使用,当所有测试位为0时,EQ有效,而只要有一个测试位不为0,则NE有效。
16. teq
TEQ相等测试指令,用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的异或运算,并根据运算结果更新CPSR中的条件标志位。指令用于比较两个操作数是否相等。如果相等,则 Z = 1,否则Z = 0。指令通常和EQ、NE条件码配合使用
例:TEQ R1, R2
TST R1,#%1;测试R1中是否设置了最低位(%表示二进制数)
17. cmn
CMN -- 比较取负的值
CMN{条件}{P} <op1>, <op2>
status = op1 - (-op2)
CMN R0, #1 @把R0与-1进行比较
18. bic
BIC指令用于清除操作数1的某些位,并把结果放置到目的寄存器中,如果在掩码中设置了某一位,则清除这一位。未设置的掩码位保持不变。
例:BIC R1, R1, #0X0F ;将R1的低四位清零,其他位不变。