曾经学习过8086/8088和51中的汇编语言,汇编语言虽然基本语法相似,但是汇编语言是不能跨平台的。不同内核的CPU所对应的汇编语言也不相同。
对于机器指令来说,CPU能够很轻松的识别并执行,但是这没有可读性,并不利于维护,这就出现了用助记符来替代操作码的汇编语言(实质就是用助记符对机器语言的映射)。不同的CPU,他的指令集不同,那么他的汇编语言也就不同。将汇编语言转换成机器语言就是汇编器的功能。
ARM标准汇编
只适用于ARM公司的汇编器,适合在Windows平台上使用,如MDK
GNU汇编
使用多种架构处理器的汇编器,多用于Linux平台编译工具链的汇编器
本质上都是针对ARM指令的
1.指令
编译完成后作为一条指令存放在内存单元中,在CPU执行时完成一定的操作
2.伪操作
编译完成后不会产生任何代码,也不会占用内存,只是在编译的时候告诉编译器该怎么编译
3.伪指令
本身不是指令,编译器在编译的时候将其替换成CPU能够识别的指令
【1】数据处理指令
指令格式:<指令>{}{ } Rd, Rn,({}:表示该项可选)
指令:机器指令码对应的汇编指令助记符
:指令执行的条件码
:状态标志,标识指令执行结果是否影响CPSR
Rd:目标寄存器
Rn:第一操作数(或寄存器)
shifter_oprand2:第二操作数数据搬移指令:mov、mvn
算术运算: add、adc、sub、sbc、rsb、rbc、mul、mla
逻辑运算: and、orr、eor、bic
移位: lsl、lsr、asr、ror
比较: cmp、cmn
测试: tst、teq
【2】跳转指令: b/bl{} addr
b、bl
【3】load/store内存传输指令:单寄存器传输、批量寄存器传输
【4】状态寄存器传送指令:msr、mrs
【5】异常中断指令:swi
【6】协处理器指令:mcr p15,0,r0,c12,c0,0 寄存器数据到协处理器数据传送
MCR{条件} 协处理器编码,协处理器操作码1,源寄存器,目的寄存器1,目的寄存器2,协处理器操作码2
mov:将数值搬移到寄存器内部或者在寄存器和寄存器之间进行数据的拷贝(常数)
mvn:将数据先取反再移动
立即数:在机器指令中存放的常数(直接放在指令中就不必在内存中存取)
1、只要在0-255之间的数就是立即数
2、如果一个数能够通过按位取反得到0-255之间的数
3、如果一个数能够通过循环右移偶数位得到0-255之间的数
算数运算
add 目标寄存器,第一操作数,第二操作数 @add r1, r0 或者 @add r2, r0, r1
adds :计算的结果如果出现进位能操作CPSR,让C置1
adc:计算的结果也把CPSR的C加上
sub,subs,subc为减法指令 @sub r2, r0, r1 @r2 = r0 - r1
mul 乘法指令 @mul r2, r1, r0
mla 乘加指令 @mla r2, r0, r1, r3 @; r2 = r1 * r0 + r3,
按位与,或,异或:and,orr,eor
@and r2, r1, r0 @;r2 = r1 & r0,按位与运算,等价于C语言中&
@orr r2, r1, r0 @;r2 = r1 | r0,按位或运算
@eor r2, r1, r0 @;r2 = r1 ^ r0,按位异或运算
按位清零 (就是说对置一的位进行清零操作)
@bic r0, #0xf @; r0 = r0 & ~(0xf<<0),按位清0操作
移位
逻辑左移:lsl 存储寄存器 操作寄存器 @lsl r1, r0, #4 @;r1 = r0 << 4,逻辑左移
逻辑右移:@lsr r1, r0, #4
算数右移:@asr r1, r0, #4
循环右移:@ror r1, r0, #4
比较:
汇编中的每一条指令都自带条件标志(就相当于自带判断语句)
比较指令:cmp
结合条件位进行使用,常见条件位:
eq:比较是否相等
ne:比较是否不等
lt:有符号数的小于
gt:有符号数的大于
le:有符号数的小于等于
ge:有符号数的大于等于
@cmp r0, r1 @;cpsr = r0 - r1,但是运算结果只影响CPSR,不保存
@cmn r0, r1 @;cpsr = r1 - r0,但是运算结果只影响CPSR,不保存
比较r0,r1寄存器值,用大减小,差保存在r2
@subgt r2, r0, r1 @if(r0 > r1) r2 = r0 - r1
@rsblt r2, r0, r1 @if(r1 > r0) r2 = r1 - r0
位测试指令:相等测试(teq)、位测试(tst)
(这两个指令用于把一个寄存器Rn的内容和另一个操作数operand2按位进行与运算,并根据运算结果更新CPSR中条件标志位的值)
@teq r0, r1 @;cpsr_z = r0 ^ r1,相等测试
@tst r0, r1, lsl #11 @;cpsr_z = r0 & 3,位测试指令
b + 跳转标签:不带返回值的跳转,一旦跳转不可返回
bl + 跳转标签:带返回值的跳转,相当执行两个步骤:LR = PC - 4, PC = PC + 偏移量
PC-4和流水线技术是相关的,PC永远保存的是取址时的地址,取址之后有译码和执行,
当执行被打断时,PC应该指向的是取址之前的地址。
其实真正的执行为: 执行 译码 取址 → 执行 译码 取址
↑PC 被打断 ↑PC
跳转的范围是 +- 32m的
b的跳转是相对跳转
所以先按偏移量左移两位,符号扩展(32位指令中只有24位是地址的偏移量,就相当于多了两位,就是26位,相对跳转的标识位除去一位,那一位就是符号位,即为25位的地址,就为2^5M的地址(2^25的地址))
CPSR读写指令:针对CPSR寄存器的读和写操作
s: 表示CPSR, r:表示寄存器
mrs:读取CPSR值
msr:写入到CPSR
注意:对CPSR操作流程:先读取旧的值,在针对读的值进行修改,最后回写
^的作用:在目标寄存器中有pc时,会同时将spsr写入到cpsr,一般用于从异常模式返回。
单寄存器操作指令:ldr/str,一次只能读写1个寄存器(4字节)
将3存放在CPU内部寄存器r1,然后将r1,写到内存地址为0x40000000
格式:
ldr/str 寄存器, [地址]
*/
@mov r0, #0x40000004
@mov r1, #3
@str r1, [r0]
@ldr r2, [r0]
@str r1, [r0, #4] @;前索引:在进行读写之前,先调整地址,再将对调整后的地址空间进行读写操作
@str r1, [r0], #4 @;后索引:先进行读写操作,之后再调整地址,并且将新的地址写入到相关寄存器
@str r1, [r0, #-4]! @;自动索引:先更新地址索引,然后再进行读写操作,最后将更新后的地址索引回写到相关寄存器
/*
批量寄存器操作指令:ldm/stm
用法:
ldm/stm 地址, {寄存器列表}
注意:
1、寄存器列表中的排序和最终于内存之间的对应关系永远是:低编号寄存器对应地地址
2、当寄存器列表中的寄存器编号连续,可以直接以寄存器区间形式写
3、需要对地址进行更新时,只要在地址寄存器后边加!
4、对于一个内存块来说,操作方法有多种:
d:表示地址递减
i:表示地址递增
a:表示after,先读写在更新 对于空空间使用
b:表示before,先更新在读写 对于满空间使用
stmdb
stmda
stmia
stmib
5、对于内存块进行写操作和进行读操作
地址延伸方向相反:以递减方式写入,以递增方式读取,索引更新相反:写入的时候,在写入之前更新索引(before)的,在读取的时候,先读取数据,在更新索引(after)
6、对于栈进行写操作和进行读操作
分为满增栈,满减栈,空增栈,空减栈
fi fd ei ed
/*
ldr r0, =0x40000000
mov r1, #1
mov r2, #2
mov r3, #3
mov r4, #4
@stmdb r0!, {r1 - r4}
@ldmia r0!, {r5-r8}
stmfa r0!, {r1-r4}
ldmfa r0!, {r5-r8}
*/
软中断指令:SWI{
/*异常向量表*/
b reset_handle
b undef_handle
b swi_handle
b pre_handle
b data_abort_handle
mov r0, r0 //一条指令的长度
b irq_handle
b fiq_handle
reset_handle:
/*初始化SVC模式下C语言运行环境:初始化栈 算是手动分配栈*/
ldr r0, =stack_end
mov sp, r0
/*切换工作模式为user*/
mrs r0, cpsr
bic r0, #0xff
orr r0, #0x10
msr cpsr, r0
/*初始化user模式下的C运行环境:栈初始化*/
ldr r0, =user_stack_end
mov sp, r0
/*模拟用户程序:计算1 + 1 = 2*/
mov r0, #1
mov r1, #1
swi #16
add r2, r1, r0
undef_handle:
swi_handle:
stmfd sp!, {r0-r2,lr} @;压栈保护现场
mov r0, #2
mov r1, #3
add r2, r1, r0
mov r0, lr
sub r0, #4 @;获取SWI机器指令地址
ldr r1, [r0] @;读取SWI机器指令
bic r1, #0xff000000 @;读取SWI中断号
mov r0, r1
bl swi_c_handle @;调用更加详细的C语言处理程序
ldmfd sp!, {r0-r2,pc}^ @;出栈还原、跳转、恢复CPSR
@ movs pc, lr
pre_handle:
data_abort_handle:
irq_handle:
fiq_handle:
stack_head:
.space 128
stack_end:
user_stack_head:
.space 128
user_stack_end:
.global func
func:
add r2, r1, r0
stop:
b stop
.end
协处理器:协助处理器完成一定操作的硬件电路
协处理器指令:协处理器执行的指令
ARM 微处理器可支持多达 16 个协处理器,用于各种协处理操作,在程序执行的过程中,每个协处理器只执行针对自身的协处理指令,忽略ARM处理器和其他协处理器的指令。ARM 的协处理器指令主要用于:
a、ARM处理器初始化
b、ARM协处理器的数据处理操作
c、ARM 处理器的寄存器和协处理器的寄存器之间传送数据
d、ARM协处理器的寄存器和存储器之间传送数据。
其他
空堆栈
当堆栈指针指向最后压入堆栈的数据时,称为满堆栈(Full Stack),而当堆栈指针指向下一个将要放入数据的空位置时,称为空堆栈(Empty Stack).
满堆栈亦然。
包含伪指令和伪操作:
伪指令:本身不是指令,只是在编译的时候由编译器替换成CPU能够识别的指令
伪操作:本身不是指令,也不占用内存,只是指示编译器该怎么编译
数据定义伪操作一般用于为特定的数据分配存储单元,同时可完成已分配存储单元的初始化。常见的数据定义伪操作有如下几种:
.byte 单字节定义 .byte 0x12,’a’,23
.short 定义双字节数据 .short 0x1234,65535
.long /.word 定义4字节数据 .word 0x12345678
.quad 定义8字节 .quad 0x1234567812345678
.float 定义浮点数 .float 0f3.2
.string/.asciz/.ascii 定义字符串 .ascii “abcd\0”,
.ascii 伪操作定义的字符串需要每行添加结尾字符‘\0‘,其他不需要
汇编控制伪操作用于控制汇编程序的执行流程,常用的汇编控制伪操作包括以下几条:
.if、.else .endif伪操作能根据条件的成立与否决定是否执行某个指令序列。当.if后面的逻辑表达式为真,则执行.if后的指令序列,否则执行.else后的指令序列; .if、.else、.endif伪指令可以嵌套使用。
.macro,.endmmacro伪操作可以将一段代码定义为一个整体,称为宏指令,然后就可以在程序中通过宏指令多次调用该段代码。其中,$标号在宏指令被展开时,标号会被替换为用户定义的符号。宏操作可以使用一个或多个参数,当宏操作被展开时,这些参数被相应的值替换。宏操作的使用方式和功能与子程序有些相似,子程序可以提供模块化的程序设计、节省存储空间并提高运行速度。但在使用子程序结构时需要保护现场,从而增加了系统的开销,因此,在代码较短且需要传递的参数较多时,可以使用宏操作代替子程序,语法格式:
.macro
{$label} macroname {$parameter{,$parameter}…}
……..code
.endm
LDR伪指令: LDR伪指令装载一个32位的常数或一个地址到寄存器。
编译器编译时将ldr伪指令翻译成ldr指令
ldr r0, =stack_end伪指令翻译为:
LDR R0,[PC,#0x0150]
nop伪指令:指的是CPU不做任何操作
nop伪指令翻译为:mov r0, r0
.arm .arm 定义一下代码使用ARM指令集编译
.thumb .thumb 定义一下代码使用Thumb指令集编译
.section .section expr 定义一个段。expr可以使.text .data. .bss
.text .text {subsection} 将定义符开始的代码编译到代码段
.data .data {subsection} 将定义符开始的代码编译到数据段,初始化数据段
.bss .bss {subsection} 将变量存放到.bss段,未初始化数据段
.align .align{alignment}{,fill}{,max} 通过用零或指定的数据进行填充来使当前位置与指定边界对齐
.org .org offset{,expr} 指定从当前地址加上offset开始存放代码,并且从当前地址到当前地址加上offset之间的内存单元,用零或指定的数据进行填充
_start 汇编程序的缺省入口是_ start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点.
.global/ .globl :用来声明一个全局的符号
.end 文件结束
.include 格式:.include “filename”
包含指定的头文件, 可以把一个汇编常量定义放在头文件中
.equ 格式:.equ symbol, expression
把某一个符号(symbol)定义成某一个值(expression).该指令并不分配空间.(c语言的 #define)
.zero size 分配size大小内存并用0初始化 .zero 400
.space size {value} 分配size大小内存空间,并用value初始化 .space 10 0xff
汇编和C程序之间相互调用规则:
1)寄存器R0-R3用来传递参数(按顺序),参数个数超过4个的,使用栈来传递
2)返回时,返回值宽度是32bit以内的,使用R0传递,64bit的用R0(低32bit)、R1传递,以此类推
3)对于栈的使用,规定用FD满减栈
C和汇编程序相互调用,必须满足ATPCS/AAPCS规则
参数、返回值传递,遵循ATPCS规则,汇编直接调用即可
汇编start.s代码:
.global _start
.equ Ni 20 @//阶乘数为20
.text
_start:
mov r0, #Ni @//通过r0传递参数
bl func
stop:
b stop
.end
C语言func.c代码:
long long func(char N)
{
char i=0;
long ret = 1;
while(i++ < N)
{
ret *= i;
}
return ret;
}
汇编程序中:
对要导出的函数标号做export(或者.global)声明,例如:export function,function没有传参数和返回值
在C程序中:
做函数体extern 声明:extern void function(void);
extern void function (char a, char b); //声明是外部函数
int main(void)
{
char a=1, b=2;
function(1, 2);//调用汇编
while(1);
}
GNU下的func.s汇编代码:
.global function
function:
mov r2, r0 @//获取参数1
mov r3, r1 @//获取参数2
add r4, r2, r3 @//计算结果
mov pc, lr
使用ldr/str指令读写访问变量,例如:ldr/str r0, a,不同的变量宽度使用不同的指令
和程序调用类似
GNU编译环境下:
__asm__ {
"" @//汇编代码
:output @//输出寄存器列表
:input @//输入寄存器列表
:change @//修改的寄存器列表
}
说明:
1)如果害怕编译器优化内嵌汇编代码,在__asm__后加__volatitle__防止编译器优化
2)汇编代码必须放在一个""中间,并且""中间不能有enter回车换行,只能用\n换行,\t为指
令格式对齐,编译器不会检查内嵌汇编语法,而是直接交给汇编器完成
3)输出寄存器列表:ASM汇编--->C语言
4)输入寄存器列表:C语言--->ASM汇编
5)告诉编译器你修改过的寄存器,编译器会自动把保存这些寄存器值的指令加在内嵌汇编之前
,再把恢复寄存器值的指令加在内嵌汇编之后
注意:
1)输出寄存器列表
“constraint约束条件”(variable变量)
约束条件:变量的存放位置
r -》 使用任何通用的寄存器
m -》 使用变量的内存地址
输出修饰符
+ -》 可读可写
= -》 只写
& -》 操作数不能与输入部分使用的寄存器重复,只能用“+&”或者“=&”的方
式
变量: 输出变量参数
2)输入寄存器列表
“constraint约束条件”(variable/immediate):
约束条件:定义变量或立即数的存放位置
r -》 使用任何可用的通用寄存器
m -》 使用变量的内存地址
i -》 使用立即数(不能用变量)
3)占位符:在汇编代码中,使用到输入输出变量或立即数的时候,使用占位符:“%N”,在输出
和输入列表中,标号顺序从左到右,从上到下,依次从0开始递增