嵌入式学习——ARM学习(2)——汇编学习

工具:Keil-uVision5

1、汇编

1.1 汇编的组成

**指令:**汇编语言的核心部分,表示 CPU 可以执行的操作,如数据传输、算术运算、逻辑运算等。

**操作数:**指令中用于指定操作对象的数据,可以是寄存器、内存地址或立即数。

**标签:**用于标识代码中的特定位置,通常用于跳转和循环控制。

**伪指令:**不直接对应于机器指令的指令,用于控制汇编过程,如 .data、.text、.global 等。

**注释:**用于解释代码,通常以特定符号(如 ;、@)开头和/* */,汇编器在处理时会忽略这些内容。

**段:**汇编程序通常分为不同的段,如代码段(.text)、数据段(.data)和堆栈段(.bss),每个段有不同的用途。

**宏:**一种用于简化代码的工具,可以定义一组指令并在需要时调用。

这些组成部分共同构成了汇编语言,使得程序员能够以更接近人类理解的方式编写与机器语言相对应的代码

1.2 汇编指令格式

  1. **标签(可选)**:
  • 用于标识指令的位置,后面跟一个冒号(`: `)。

  • 例如:`loop_start:`

  1. **操作码(Opcode)**:
  • 指令的名称,表示要执行的操作。

  • 例如:`MOV`、`ADD`、`SUB`。

  1. **操作数**:
  • 指令的参数,指定操作的对象,可以是寄存器、立即数或内存地址。

  • 例如:`R0`、`R1`、`#5`。

  1. **条件码(可选)**:
  • 用于指定指令的执行条件,通常放在操作码前。

  • 例如:`EQ`(相等)、`NE`(不相等)。

  1. **注释(可选)**:
  • 用于解释指令的功能,通常以 `@` 开头,汇编器会忽略这些内容。

  • 例如:`@ 这是一个注释`。

示例格式:

```assembly
label: opcode operand1, operand2 @ 注释

```

示例指令:

```assembly

loop_start: MOV R0, #5 ; 将 5 移动到 R0 寄存器

ADD R1, R0, #10 ; 将 R0 和 10 相加,结果存储到 R1

```

在这个示例中:

  • `loop_start` 是标签。

  • `MOV` 和 `ADD` 是操作码。

  • `R0`、`R1` 和 `#10` 是操作数。

  • 注释解释了每条指令的功能。

2、汇编指令

2.1 数据搬移指令

2.1.1 MOV指令

格式:

mov{条件码} 目标寄存器,操作数

解释:

条件码可有可无,当存在条件码时,如果条件满足,则将操作数数值搬移到目标寄存器

主要功能:

1、将数据从一个寄存器传送到另一个寄存器。

2、将一个常数值传送到寄存器中。

3、当 PC( R15)用做目的寄存器时,可以实现程序跳转。如" MOV PC, LR",所以这种跳转可以实现子程序调用及从子程序返回,代替指令" B, BL"。

4、当 PC 作为目标寄存器且指令中 S 位被设置时,指令在执行跳转操作的同时,将当前处理器模式的 SPSR 寄存器的内容复制到 CPSR 中。这种指令" MOVS PC LR"可以实现从某些异常中断中返回。

eg:

MOV R0, #5 ; 将立即数 5 移动到 R0

2.1.2 MVN 指令

格式:

mvn{条件码} 目标寄存器,操作数

解释:

将操作数按位取反后的结果搬移到目标寄存器中

主要功能:

1、MVN 是反相传送( Move Negative)指令。它将操作数的反码传送到目的寄存器。

2、MVN 指令多用于向寄存器传送一个负数或生成位掩码。

3、MVN 指令将 shifter_operand 表示的数据的反码传送到目的寄存器 Rd,并根据操作结果更新 CPSR 中相应的条件标志位。

主要作用:

1、向寄存器中传送一个负数。

2、生成位掩码( Bit Mask)。

3、求一个数的反码。

eg:

MVN R0, #0xfffffffe ; 把0x00000001 移动到 R0

2.1.3立即数

在 ARM 汇编语言中,立即数(Immediate Value)是指在指令中直接给出的常量值,而不是存储在寄存器或内存中的值。立即数通常用于算术运算、逻辑运算和数据传输等操作。

数据的处理方式

假设存在立即数x ,将x循环右移偶数位y ,将会得到一个0-255内的数据z .此时,就说明x 是一个立即数,当x 当作指令的一部分执行时,实际保存到指令空间的时数据y 和数据z ,在指令空间的**[7:0]保存的是数据z** ,[11:8]保存的是数据(32-y)/2.

立即数的特点

  1. 直接使用:立即数可以直接在指令中使用,无需先加载到寄存器。

  2. 范围限制:在 ARM 中,立即数的表示通常有范围限制,具体取决于指令的类型。例如,某些指令的立即数可以是 8 位、12 位或 16 位。

  3. 前缀:在 ARM 汇编中,立即数通常以 # 符号开头。

立即数的判断:

1.直接写完编译,出错就不是立即数

2.判断一个数据是不是立即数时,只需要判断这个数据 或者它的取反值能不能循环右移偶数位得到一个0-255内的数据,如果能吗,这个数据就是一个立即数
如果想要将一个非立即数放到寄存器中,可以使用一个伪指令LDR

格式:ldr 目标寄存器,=数据

eg: LDR R0,=0X12345678

2.2 移位运算指令

2.2.1 LSL 逻辑左移

解释:

一个32位数据进行逻辑左移,最高位移出,最低位补0

格式:

lsl{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

MOV R0, #0XFF

LSL R1, R0, #0X4 @0xFF0

C语言语法也可以

LSL R1, R0, #(0x1<<2) @0xFF0

2.2.2 LSR 逻辑右移

解释:

一个32位的数据进行逻辑右移,最低位移出,最高位补0

格式:

lsr{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

MOV R0, #0XFF

LSR R1, R0, #0X4 @0xF

2.2.3 ASR 算数右移

解释:

一个32位的数据进行逻辑右移,最低位移出,最高位(30)补0(31符号位不变)

格式:

asr{条件码} 目标寄存器,第一操作寄存器,第二操作数

2.2.4 ROR 循环右移

解释:

一个32位的数据进行循环右移,最低位移出,补到最高位

格式:

ror{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

MOV R0, #0XFF

ROR R1, R0, #0X4 @0xF000000F

2.3 位运算指令

2.3.1 and 与运算

解释:

将第一操作寄存器的数据和第二操作数进行与运算,得到的结果保存到目标寄存器中

格式:

and{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

and r2,r1,r3 ;r2 = r1&r3

2.3.2 orr 或运算

解释:

将第一操作寄存器的数据和第二操作数进行或运算,得到的结果保存到目标寄存器中

格式:

orr{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

orr r2,r1,r3 ;r2 = r1 | r3

2.3.3 eor 异或运算

解释:

将第一操作寄存器的数据和第二操作数进行异或运算,得到的结果保存到目标寄存器中

格式:

eor{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

eor r2,r1,r3 ;r2 = r1 ^ r3

2.3.4 mvn取反运算

查看2.1.2

2.3.5 bic 按位清零(遇1清0)

解释:

将第一操作寄存器的数据和第二操作数进行按位清零运算,得到的结果保存到目标寄存器

格式:

BIC{条件码} 目标寄存器,第一操作寄存器,第二操作数

eg:

bic r0, r0, #0x1011 ;清除r0中的位12、 4和0位,保持其余的不变

bic r1, r2, r3 ;将 r3 和 r2 做"逻辑与"操作,结果保存到 r1 中

2.4 算数运算指令

2.4.1 add 加法

解释:

将第一操作寄存器的数值+第二操作数,结果保存到目标寄存器中,如果+s,运算结果会影响到CPSR条件位

格式:

add{条件码}{s} 目标寄存器,第一操作寄存器,第二操作数

2.4.1.1 adc 加法

解释:

目标寄存器 = 第一操作寄存器 + 第二操作数 + cpsr的c位数值

格式:

adc{条件码} 目标寄存器,第一操作寄存器,第二操作数

2.4.2 sub 减法

解释:

将第一操作寄存器的数值-第二操作数,结果保存到目标寄存器中,如果+s,运算结果会影响到CPSR条件位

格式:

sub{条件码}{s} 目标寄存器,第一操作寄存器,第二操作数

2.4.2.1 sbc 减法

解释:

目标寄存器 = 第一操作寄存器 - 第二操作数 - cpsr的c位的取反值

格式:

sbc{条件码} 目标寄存器,第一操作寄存器,第二操作数

2.4.3 rsb 翻转减

解释:

将第二操作数-第一操作寄存器的数值,结果保存到目标寄存器中,如果+s,运算结果会影响到CPSR条件位

格式:

rsb{条件码}{s} 目标寄存器,第一操作寄存器,第二操作数

2.4.4 mul 乘法

解释:

将第二操作数*第一操作寄存器的数值,结果保存到目标寄存器中

格式:

mul{条件码}{s} 目标寄存器,第一操作寄存器,第二操作数

2.5 比较指令

cmp 比较

解释:

将第一操作寄存器的数值和第二操作数进行比较 比较指令的本质是比较的两个数据进行减法运算,并且运算的结果会影响到CPSR寄存器的条件位

格式:

cmp 第一操作寄存器,第二操作数

注意:

比较指令经常和条件码一起使用,即先比较两个数的大小,再下面执行的指令后加上条件码,根据条件码 对应的条件是否满足决定这条指令是否执行

图表:

2.6 跳转指令

2.6.1 b

解释:

跳转到指定的标签下执行,当程序发生跳转时lr不保存程序的返回地址

格式:

b 标签

2.6.2 bl

解释:

跳转到指定的标签下执行,当程序发生跳转时lr保存程序的返回地址

格式:

bl 标签

2.7 内存读写指令

通过指令将一个寄存器的数据向内存中写或者从内存中读取数据保存到一个寄存器中

2.7.1 单寄存器内存读写指令 STR / LDR

向内存中写 str

1、str 目标寄存器,[保存目标地址的寄存器] @将目标寄存器4字节数据写入到目标地址对应的内存中

2、strh 目标寄存器,[保存目标地址的寄存器] @将目标寄存器2字节数据写入到目标地址对应的内存中

3、strb 目标寄存器,[保存目标地址的寄存器] @将目标寄存器1字节数据写入到目标地址对应的内存中

从内存中读 ldr

1、ldr 目标寄存器,[保存目标地址的寄存器] @从目标地址对应的内存中读取4字节数据写入到目标寄存器

2、ldrh 目标寄存器,[保存目标地址的寄存器]@从目标地址对应的内存中读取2字节数据写入到目标寄存器

3、ldrb 目标寄存器,[保存目标地址的寄存器] @从目标地址对应的内存中读取1字节数据写入到目标寄存器

拓展:

str 目标寄存器,[保存目标地址的寄存器,#操作数] @将目标寄存器的数据写入到目标地址+操作数为首地址的内存中

ldr 目标寄存器,[保存目标地址的寄存器,#操作数] @从目标地址+操作数为首地址的内存中读取数据写入到目标寄存器

str 目标寄存器,[保存目标地址的寄存器],#操作数 @将目标寄存器的数据写入到目标地址为首地址的内存中,然后保存目标地址的寄存器数值变为原来的数据+操作数大小

ldr 目标寄存器,[保存目标地址的寄存器],#操作数 @从目标地址为首地址的内存中读取数据写入到目标寄存器,然后保存目标地址的寄存器数值变为原来的数据+操作数大小

2.7.2 批量寄存器内存读写指令 STM / LDM

**写:**将多个寄存器的数据写入到指定内存中

**读:**从指定的内存中读取数据写入到多个寄存器

2.7.2.1 stm / ldm

写:

stm 保存目标地址的寄存器,{寄存器列表} @将寄存器列表中各个寄存器的数据写入到目标地址对应的内存中

读:

ldm 保存目标地址的寄存器,{寄存器列表} @从目标地址对应的内存中读取数据保存到寄存器列表中的各个寄存器中

注意:

1.寄存器列表中寄存器编号如果连续,可以用-表示一个寄存器范围 {r1-r5} 如果寄存器编号散乱,则用 ,分隔每一个寄存器 {r1,r3,r5,r7} {r1-r3,r5,r7}

2.无论寄存器列表中寄存器的顺序是什么样的,低内存地址始终和小编号寄存器对应

2.7.2.2 地址增长方式

当指定一个内存基地址后,我们对这个基地址内存进行读写操作,然后根据地址增长方式可以让我们操作的基地址发生变化

格式:

stm{地址增长后缀} 保存目标地址的寄存器!,{寄存器列表}

解释:

将寄存器列表中的寄存器数据写入到目标地址中时,保存目标地址的寄存器保存的目标地址会发生对应的改变

后缀:

**ia:**先向目标地址对应的内存中写入数据,然后保存目标地址的寄存器保存的目标地址数值+4

ib: 先让保存目标地址的寄存器保存的地址+4,再向目标地址对应的内存中写入数据 **da:**先向目标地址对应的内存中写入数据,然后保存目标地址的寄存器保存的目标地址数值-4

**db:**先让保存目标地址的寄存器保存的地址-4,再向目标地址对应的内存中写入数据

ldm同上

2.7.3 栈内存读写指令

操作栈内存需要知道栈顶的地址,而栈顶地址保存在SP寄存器中

栈类型

增栈:每次压栈结束后栈指针寄存器保存的栈顶地址往高地址方向增长

减栈:每次压栈结束后栈指针寄存器保存的栈顶地址往低地址方向增长

空栈:每次压栈结束后栈指针寄存器指向的栈顶空间没有保存的有效数据

满栈:每次压栈结束后栈指针寄存器指向的栈顶空间存在有效数据

栈可以分为空增栈(EA)、空减栈(ED)、满增栈(FA)、满减栈(FD)
ARMv7-a架构处理器默认使用满减栈实现压栈和出栈

压栈命令:

push {寄存器列表} @将寄存器列表中每一个寄存器的数值进行压栈

出栈命令:

pop {寄存器列表}

手动实现满减栈

方法1、db后缀压栈 ia后缀出栈

eg:

方法2、使用满减栈对应的后缀fd实现

eg:

2.8 状态寄存器传送指令 MRS / MSR

内核中存在一个寄存器CPSR,这个寄存器保存的是当前程序的状态,我们可以通过状态寄存器传送指令实现对当前程序状态的读取以及修改程序的状态

MRS 目标寄存器,CPSR

@读取CPSR保存的状态值到目标寄存器中

MSR CPSR,操作数

@将操作数写入到CPSR寄存器中进而修改程序的状态

注意:

在特权模式下可以通过修改cpsr寄存器的数值切换到USER模式

但是USER模式下无法通过修改CPSR数值切换到特权模式

想要在user模式下切换到其他模式,只要发生对应的事件后处理器自动进入对应的模式

2.9 软中断产生指令

2.9.1 软中断

swi 中断号

@中断号是一个12位的立即数,不同的中断号用来标识不同的中断

2.9.2 异常模式和异常源

**异常模式:**当发生了异常之后处理器进入的工作模式就是异常模式

**异常源:**引发处理器进入异常模式的源头叫做异常源

5种 异常模式和7种异常源

|-------|------------------|
| 异常模式 | 异常源 |
| IRQ | 产生了一个IRQ中断 |
| FIQ | 产生了一个FIQ中断 |
| UNDEF | 处理器遇到未定义指令导致程序中止 |
| SVC | 复位 |
| SVC | 产生软中断 |
| ABORT | 存取数据异常导致程序中止 |
| ABORT | 存取指令异常导致程序中止 |

2.9.3 异常向量表

在 ARM 架构中,异常向量表(Exception Vector Table)是一个特殊的内存区域,用于处理各种异常和中断。异常向量表的每个条目对应于特定类型的异常,指向处理该异常的代码(即异常处理程序)。

异常向量表的结构

在 ARM 体系结构中,异常向量表通常位于内存的起始地址(例如,地址 0x00000000)。每个异常向量的大小通常为 4 字节(32 位),因此每个异常向量表的条目可以容纳一个指向异常处理程序的地址。

|------------|---------|-------|
| 地址 | 异常 | 进入模式 |
| 0x00000000 | 复位 | 管理模式 |
| 0x00000004 | 未定义指令 | 未定义模式 |
| 0x00000008 | 软件中断 | 管理模式 |
| 0x0000000C | 中止(预取) | 中止模式 |
| 0x00000010 | 中止(数据) | 中止模式 |
| 0x00000014 | 保留 | 保留 |
| 0x00000018 | 中断 IRQ | 中断模式 |
| 0x0000001C | 快中断 FIQ | 快中断模式 |

bash 复制代码
.section .vectors
    .word _reset_handler      ; 复位向量
    .word _undefined_handler   ; 未定义指令向量
    .word _swi_handler         ; 软件中断向量
    .word _prefetch_abort_handler ; 预取异常向量
    .word _data_abort_handler  ; 数据异常向量
    .word 0                    ; 保留
    .word _irq_handler         ; IRQ 向量
    .word _fiq_handler         ; FIQ 向量

.section .text
_reset_handler:
    ; 复位处理代码
    B main                    ; 跳转到主程序

_undefined_handler:
    ; 未定义指令处理代码
    B .

_swi_handler:
    ; 软件中断处理代码
    B .

_prefetch_abort_handler:
    ; 预取异常处理代码
    B .

_data_abort_handler:
    ; 数据异常处理代码
    B .

_irq_handler:
    ; IRQ 处理代码
    B .

_fiq_handler:
    ; FIQ 处理代码
    B .

2.9.4 异常处理流程(四大步,三小步)

1、保存程序当前的工作状态到对应的异常模式的SPSR寄存器中

2、修改SPSR

1.修改程序的工作模式为对应的异常模式[4:0]

2.修改程序的工作状态为ARM状态[5] @ARM为0,thumb为1

3.根据当前异常的优先级设置程序合适的中断禁止[7:6] @1为忽略

3、将程序的返回地址保存到对应的异常模式的LR寄存器(L14)

4、修改PC寄存器的值为当前异常在异常向量表中的位置

注意:该步骤为处理器自动完成

2.9.5 异常返回工作

注意:该步骤为 程序员手动完成

1、恢复程序状态 SPSR => CPSR

2、返回主程序执行 LR => PC

2.9.6 测试代码

bash 复制代码
.text  
.global _start 
            

_start:
@初始化异常向量表
    b main
    b .
    b do_swi  @产生软中断后跳到这里执行
    b .
    b .
    b .
    b .
    b .
main:
@初始化SVC模式下的栈
    ldr sp,=0X40000020
    @切换程序的工作模式到user模式
    MSR CPSR,#0x10
    @初始化user模式下的栈
    ldr sp,=0X40000020
    @主程序执行
    mov r1,#1
    mov r2,#2
    @产生一个软中断
    swi 1
    add r3,r1,r2
    b loop
    

@软中断异常处理程序
do_swi:
@压栈保护现场
    stmfd sp!,{r1,r2,lr}
    mov r1,#3
    mov r2,#4
    mul r4,r1,r2
    ldmfd sp!,{r1,r2,pc}^  @异常返回
    
loop:
    b loop
.end 
    

3、混合编程

要想实现C和汇编的混合编程必须遵循ATPCS规范。

ATPCS : ARM-Thumb Procedure Call Standard

bash 复制代码
int add(int i,int j) {
    return i+j;
}

将汇编的标签当作C语言的函数使用

将C语言的函数当作汇编的标签使用

函数参数的传递采用R0-R3进行传递,如果参数的个数大于4个通过压栈的方式进行传递

函数的返回值通过R0 返回,如果函数的返回值大于4个字节 通过r0-r1返回

ATPCS规范中规定ARM采用满减栈。

3.1 汇编调用C语言的函数

cs 复制代码
#include <stdio.h>

// 声明汇编函数
extern int add_func(int a, int b);

int main() {
    int result = add_func(5, 10); // 调用汇编函数
    printf("Result: %d\n", result);
    return 0;
}
cs 复制代码
*****汇编文件**********
.text    
.global _start  
            
_start: 

    @ 1. 初始化栈指针,C代码运行必须有栈
    ldr sp, =0x40000820
    
    @ 2. 汇编调用c函数 
    @ 2.1 给C的函数传递实参值
    mov r0, #3   @ a = 3
    mov r1, #4   @ b = 4
   
    @ 2.2 汇编调用c的函数
    bl add_func
    
    @ 2.3 函数的返回通过r0返回,查看r0寄存器中的值

loop:   
        b loop  

.end

3.2 c语言调用汇编标签

cs 复制代码
.text    
    .globl _start  
                
    _start: 
    
        @ 1. 初始化栈指针,C代码运行必须有栈
        ldr sp, =0x40000820
        
        @ 2. 汇编调用c,跳转到main函数
        b main
    .end
cs 复制代码
********c文件************
// 使用extern对函数进行声明
extern int add_func(int a, int b, int c, int d);

int sum = 0;
int main()
{
 // 在c代码中调用汇编代码
 sum = add_func(1,2,3,4);
 while(1);
 return 0;
}
cs 复制代码
********汇编文件**********
.text 
.global add_func  @ 将add_func函数声明为全局
    
    add_func:
        add r0, r0, r1
        add r0, r0, r2
        add r0, r0, r3
        mov pc, lr
.end

3.3 c语言内联汇编

c语言中直接使用汇编的语法

通过asm关键字进行修饰

cs 复制代码
asm volatile(
    "汇编指令\n\t"     //"\n\t"表示一条指令的结束
    .....
    :输出列表  //指令结果的输出值
    :输入列表  //指令的数据输入
    :破坏列表  //破坏列表指定我们当前被修改的资源 memory
);
cs 复制代码
********汇编启动文件*******
.text    
.globl _start  
            
_start: 

    @ 1. 初始化栈指针,C代码运行必须有栈
    ldr sp, =0x40000820
    
    @ 2. 汇编调用c,跳转到main函数
    b main
.end
**********c语言文件***********
 // 内联汇编 
int add_func2(int a, int b, int c, int d)
{
 int sum = 0;
 // 使用汇编实现求和
 asm volatile(  "add r0, r0, r1\n\t"  "add r0, r0, r2\n\t"  "add r0, r0, r3\n\t"  :"=r"(sum)  :"r"(a),"r"(b),"r"(c),"r"(d)  :"memory" );
 return sum;
}
 //"=r"(sum)表示输出从寄存器中放到变量sum中
// "r"(a) 指定输入从变量a中获取放到通用寄存器
 //"memory"声明使用内存

// 使用extern对函数进行声明
extern int add_func(int a, int b, int c, int d);

int sum = 0;
int main()
{
 // 调用内联汇编的函数 
 sum = add_func2(5,6,7,8);
 
 // 在c代码中调用汇编代码
 sum = add_func(1,2,3,4);
 while(1);
 return 0;
}
 *********汇编文件*************
 .text 
.global add_func  @ 将add_func函数声明为全局
    
add_func:
        add r0, r0, r1
        add r0, r0, r2
        add r0, r0, r3
        mov pc, lr
.end
    
相关推荐
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__3 小时前
Web APIs学习 (操作DOM BOM)
学习
数据的世界015 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐5 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
OopspoO7 小时前
qcow2镜像大小压缩
学习·性能优化
A懿轩A8 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒8 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
kkflash38 小时前
提升专业素养的实用指南
学习·职场和发展
1 9 J9 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
6.9410 小时前
Scala——身份证号码查询籍贯
学习·scala