目录

嵌入式ARM架构学习2汇编

嵌入式ARM架构学习2——汇编

一、处理器工作模式
ARM有7个基本工作模式:

  • User:非特权模式,大部分任务执行在这种模式
  • FIQ:当一个高优先级(fast)中断产生时将会进入这种模式
  • IRQ:当一个低优先级(normal)中断产生时将会进入这种模式
  • Supervisor:当复位或软中断指令执行时将会进入这种模式
  • Abort:当存取异常时将会进入这种模式
  • Undef:当执行未定义指令时会进入这种模式
  • System:便用和User模式相同寄存器集的特权模式

Cortex-A特有模式:

  • Monitor:是为了安全而扩展出的用于执行安全监控代码的模式;

也是一种特权模式

https://i-blog.csdnimg.cn/direct/7ff69d9065bf47139c4c0726fdfb4096.png

https://i-blog.csdnimg.cn/direct/4ca72264e9c94d4bb14138f98fcbec78.png

Part1

学习arm汇编的主要目的是为了编写arm启动代码,启动代码启动以后,引导程序到c语言环境下运行。换句话说启动代码的目的是为了在处理器复位以后搭建c语言最基本的需求。因此启动代码的主要任务有:
    1、初始化异常向量表;
    2、初始化各工作模式的栈指针寄存器;
    3、开启arm内核中断允许;
    4、将工作模式设置为user模式;
    5、完成上述工作后,引导程序进入c语言主函数执行;
    因此汇编指令的学习主要是围绕这几个目的展开,主要学习跟上述目的相关的指令。
    
1.格式
    伪操作:它们不是 ARM 处理器实际的指令(如 MOV, ADD 等),而是写给汇编器看的命令,用于指导汇编器如何工作
    area reset, code, readonly
    code32
    entry
    end    
    area: 这是最重要的一个伪操作,用于定义一个段。程序、数据、堆栈等都需要被组织在不同的段中。
    reset: 这是你为这个段起的名字。名字 reset 具有很强的暗示性,通常用于表示复位向量段,即CPU上电或复位后首先执行的第一段代码所在的位置。
    code: 指定该段的属性为代码,意味着这个段包含可执行的指令。
    readonly: 指定该段的属性为只读。对于代码段来说,这通常是默认且必须的。

    code32: 表示后续指令使用 32位的 ARM 指令集。
    thumb: 表示后续指令使用 16位的 Thumb 指令集。
2.指令  
    1.mov
        MOV{S} , #
        MOV{S} ,

        MOV instruction                Canonical form
        MOV{S} , , ASR # ASR{S} , , #
        MOV{S} , , LSL # LSL{S} , , #
        MOV{S} , , LSR # LSR{S} , , #
        MOV{S} , , ROR # ROR{S} , , #
        MOV{S} , , ASR ASR{S} , ,
        MOV{S} , , LSL LSL{S} , ,
        MOV{S} , , LSR LSR{S} , ,
        MOV{S} , , ROR ROR{S} , ,
        MOV{S} , , RRX         RRX{S} ,
        注意    (1)与C语言中的赋值运算对比(左值/右值),利于加深理解
            (2)#/ 取值范围 (0 - 31)
            (3)RRX{S}:扩展右移 (不需要移位量)
            (4)在计算机中只识别二进制数据,计算机没有有无符号,浮动点等概念;
            
        mov r0, #0x8
        mov r1, r0
        mov r3, #31

        mov r0 #1
        mov r6, r0, lsl #31
        mov r7, r0, lsl r3
        
    2.add(加法指令)
        立即数作为第二操作数:        ADD{S} , , #
        寄存器作为第二操作数寄存器:    ADD{S} , , {, }
        寄存器作为第二操作数移位量:    ADD{S} , , ,
        
        mov r0, #0x0F
        mov r1, #0xF0
        mov r2, #1

        add r6, r0, #0xF0
        add r7, r0, r1
        add r7, r0, r1, lsl #1
        add r8, r0, r1, lsl r2
        注意    (1){, } 其中{}代表可选择,“,”表示在使用时需要在Rm后添加“,” shift 移位量(立即数)
            (2) add r0, #3, #2 :为什么没有这种形式,C语言int a = 1 + 2; 编译阶段计算, 不需要在机器指令中体现 
            
    3.sub(减法指令)
        立即数作为第二操作数:        SUB{S} , , #
        寄存器作为第二操作数寄存器:    SUB{S} , , {, }
        寄存器作为第二操作数移位量:    SUB{S} , , ,
        
        mov r0, #0xFF
        mov r1, #0xF0
        mov r2, #1

        sub r6, r0, #0xF0
        sub r7, r0, r1
        sub r7, r0, r1, lsl #1
        sub r8, r0, r1, lsl r2

        以上指令都有立即数作为第二操作数的情况,那么什么是立即数呢?
            准确的说这里所指的是12位立即数imm12。先说怎么判断某数是不是12位立即数,12位立即数的条件是:
        判断标准:把某个数展开成2进制,该数必须存在一种循环右移(偶数位),使得移位后高24位全0,低8位即为有效imm8;
        原因:
            0000 (rotate)0000 0000(imm8)
            N(0-15)        1000 0001
            2N(0-30)
    4.ldr(加载指令)
        LDR ,

    6.MVN(按位取反移动指令):
        MVN{S} , #
        MVN{S} , {, }
        MVN{S} , ,

    7.bic(bit clear):指定位置清0
        BIC{S} , , #
        BIC{S} , , {, }
        BIC{S} , , ,
        
        mov r0, #0xFFFFFFFF
        mov r1, #1
        bic r2, r0, r1, lsl    #31
        bic r3, r0, #(1 « 31)
        
    8.orr(or):指定位置1
        ORR{S} , , #
        ORR{S} , , {, }
        ORR{S} , , ,
        
        mov r0, #0x00
        mov r1, #1
        mov r2, #31
        orrs r6, r0, #0x80000000
        orrs r7, r0, #(1 « 31)
        orrs r8, r0, r1, lsl #31
        orrs r9, r0, r1, lsl r2
        
    9.条件判断标志NZCV
        CPSR寄存器中条件判断标志位
        N: 符号标志位:上条指令执行结果最高位bit31为1,则 N = 1, 当结果作为有符号解释时为负值;
        Z: 零值标志位:上条指令执行结果为0(即bit0 - bit31 均为0),则 Z = 1;
        C: 进位标志位:进行无符号解读,如果在加法过程中进位或者减法时没有借位,则为 C = 1,否则 C = 0
        V: 溢出标志位:进行有符号解读,是否发生溢出 -2^31 - 2^31-1(两个正数加得负数,两个负数加得正数)
        条件码:eq ge gt le lt al(无条件执行)
        equal:等于
        not equal:不等于
        
    10.cmp(compare):比较指令
        CMP , #
        CMP , {, }
        CMP , ,
        
        cmp r0, r1 <==> subs r0, r1
        比较两个数中的最大值
        练习:比较获取三个数中最大值
        
    11.b bl bx :(跳转指令)
        B

Part2

1.循环
    (1)循环三要素
        循环结束条件
        推动循环趋向终结的语句
        循环的循环体
    (2)do…while(C)
        int i = 0;
        int sum = 0;
        do{
            sum += i;
            i++;
        }while(i <= 100)
    
        mov r0, #0
        mov r1, #0
    loop
        add r1, r1, r0
        add r0, r0, #1
        cmp r0, #100
        ble loop

    (3)while(C) for()
        int i = 0;
        int sum = 0;
        while(i <= 100)
        {
            sum += i;
            i++;
        }
        
        int sum = 0;
        for(int i = 0; i <= 100; i++)
        {
        sum += i;
        }

        mov r0, #0
        mov r1, #0
    loop
        cmp r0, #100
        bgt finish
        add r1, r1, r0
        add r0, r0, #1
        b loop    
    finish
        b finish

2.函数定义及调用
    (1)返回值 函数名(形参列表)
        {//函数体
            代码块
        }
    (2)pc => lr and lr =>pc
    
            b main              
        func
            mov r0, #1
            mov r1, #2
            add r3, r0, r1
            bx lr

        main
            mov r0, #100
            mov r1, #200
            bl func
            mov r3, #300
    (3)保护现场/恢复现场(压栈/弹栈)
        问题1:函数被调修改主调寄存器,问题二:函数嵌套时,无法正确返回。
        (1)栈类型空增 空减 满增 满减(ARM 2440))画图讲解
            空栈: *sp = xxxx        满栈: sp++
                  sp++                        *sp = xxxx 
            增栈: sp += 4           减栈: sp -= 4
        (2)栈顶指针寄存器初始化
            mov sp, #0x40001000  : 报错非立即数    
            ldr sp,    =0x40001000
            魔术棒 -> Target->IRAM1:#0x40000000 size:0x1000
        (3)保护现场 :stmfd (store(存储) multiple(多个) full(满) decrease(减少))
            STMDB {!},
            :栈顶指针寄存器
            {!},:入栈出栈后,栈顶指针寄存器自减自增
            :入栈出栈的寄存器列表
            
        (4)恢复现场 :ldmfd (load(加载) multiple(多个) full(满) decrease(减少))
            LDMFD {!},
            GNU体系 保护现场、恢复现场由主调完成
                b main    
            func1
                mov r0, #10
                mov r1, #20
                cmp r0, r1
                movge r2, r0
                movlt r2, r1
                bx lr
                    
            func0
                mov r0, #1
                mov r1, #2
                add r3, r0, r1
                stmfd sp!, {r0-r12, lr}
                bl func1
                ldmfd sp!, {r0-r12, pc}
                ;bx lr

            main
                ldr sp, =0x40001000
                mov r0, #100
                mov r1, #200
                stmfd sp!, {r0-r12, lr}
                bl func0
                ldmfd sp!, {r0-r12, lr}
                mov r3, #300

            finish
                b finish
                end    
        《《《《《《《《《《《《《《《《《《《《《《《休息》》》》》》》》》》》》》》》》》》》》》》》
        
        (4)在汇编中调用C语言函数
            (1) 创建main.c
            (2) 声明 extern void c_add(void);
            (3) 导入 import c_add; (keil当中要求)
            (4) 保护现场 bl函数调用 恢复现场
            (5) 解决编译报错:asm.axf: Error: L6238E: start.o(reset) contains invalid call from ‘~PRES8 (The user did not require code to preserve 8-byte aligment of 8-byte data objects)’ function to ‘REQ8 (Code was permitted to depend on the 8-byte aligment of 8-byte data items)’ function c_add.
                asm.axf: Finished: 0 information, 0 warning and 1 error messages.
                解决办法:栈对齐伪指令:preserve8 用于确保函数调用时栈指针保持 8 字节对齐
            (6) 创建工程自动添加了启动代码报错:
                (1)chong建覆盖工程
                (2)删除.sct文件
                (3)添加 start.s main.c 
                (4)重设软件配置
            (7) 魔术棒 -> Debug -> Use Simulator->Run to main(取消)
            (8) 魔术棒 -> Linker -> Use Memory Layout from Taget Dialog(勾选)
            (9) 魔术棒 -> Taget -> ROM1 -> Start: 0x0 Size:0x2000
            (10)函数传参:
                stmfd sp!, {r0-r12, lr}
                mov r0, #1
                mov r1, #2
                mov r2, #3
                mov r3, #4
                mov r4, #5
                stmfd sp!, {r4}
                bl c_add
                ldmfd sp!, {r4}
                ldmfd sp!, {r0-r12, lr}
        
        (5)在C语言中调用汇编    
            (1)导出 export func1;
            (2)声明 extern int func1(int a, int b);
        
        (6)修改工作模式
            CPSR M域修改
            MRS (read):    MRS , <spec_reg>
            MSR    (writ):    MSR <spec_reg>, #
                        MSR <spec_reg>,
            mrs r0, cpsr
            bic r0, r0, #(0x1FF « 0)
            orr r0, r0, #(0x10 « 0)
            mrs cpsr, r0
        (7)异常向量表    
            软中断异常:swi #7

10、ARM 内核工作模式有哪些,分别是在什么情况下被切换?(同4)

ARM内核(主要指ARMv4-v7架构的经典A/R系列)通常有7种工作模式,主要用于处理异常和提供系统保护。模式的切换主要由硬件自动完成或通过软件指令触发。

模式切换条件/触发场景
用户模式 (User)正常程序执行的非特权模式。应用程序通常运行在此模式下。
系统模式 (System)一种特权模式,与User模式共用寄存器。通过软件直接修改CPSR的模式位进入,用于运行需要特权的操作系统任务。
快速中断模式 (FIQ)当处理器接受一个快速中断 (FIQ) 请求时自动进入,用于处理高速数据传输、紧急事件等。
普通中断模式 (IRQ)当处理器接受一个普通中断 (IRQ) 请求时自动进入,用于处理一般的外设中断。
管理模式 (Supervisor)复位后的默认模式。当执行软件中断指令 (SWI/SVC) 或进行软复位时自动进入。这是操作系统的内核模式。
中止模式 (Abort)当发生内存访问失败时自动进入,例如:- 预取指中止:CPU试图从无效地址取指令。- 数据中止:指令试图访问无效的内存数据地址。
未定义模式 (Undefined)当CPU遇到一条它无法识别(未定义)的指令时自动进入。

Cortex-A特有模式: **8.Monitor:**是为了安全而扩展出的用于执行安全监控代码的模式:也是一种特权模式

9.HYP:测试

总结:除了用户模式系统模式,其他5种模式都与特定的异常一一对应。发生异常时,硬件自动切换模式;处理完异常后,通过特定的指令恢复回之前的模式。


11、异常向量表是什么?(同5)

异常向量表是内存中一块固定的、连续的区域,其中存放着各种异常处理程序的入口地址(或跳转到处理程序的指令)。

  • 工作原理:当异常发生时,ARM处理器会自动地、硬连线地根据异常类型,跳转到表中一个固定的偏移地址上。例如,发生复位异常,就跳转到0x00000000;发生IRQ中断,就跳转到0x00000018

  • 核心作用:它是连接硬件异常软件处理程序的桥梁。硬件通过固定机制自动定位处理程序,确保了异常响应的实时性和确定性

  • 典型布局(地址偏移量):

    • 0x00: 复位 (Reset)
    • 0x04: 未定义指令 (Undefined Instruction)
    • 0x08: 软件中断 (SWI/SVC)
    • 0x0C: 预取指中止 (Prefetch Abort)
    • 0x10: 数据中止 (Data Abort)
    • 0x14: 保留 (Reserved)
    • 0x18: IRQ
    • 0x1C: FIQ

12、什么是立即数?如何判断某数是非法是12位立即数?

  • 立即数:是指在指令编码本身中直接包含的操作数。执行时可以直接使用,无需从寄存器或内存中额外读取。

    • 例如:在指令 MOV R0, #0xFF 中,0xFF 就是一个立即数。
  • 判断12位立即数是否合法:ARM指令中的立即数并非完整的32位,而是由一个12位的编码字段(4位旋转值 + 8位立即数) 来表示。一个32位的常数是合法的12位立即数,当且仅当它可以通过以下方式生成:

    一个8位的数值(范围0-255),循环右移偶数位(0, 2, 4, …, 30)后得到的32位常数。

    判断方法:您可以想象这个数能否被一个“字节+移位”的组合所表示。常见的非法立即数有:

    • 过大的数(如 0x12345678
    • 所有位模式中若出现连续 1 或连续 0 的“块”长度 > 8 位,基本就编不进去,(如 0x00000FFF 是合法的,因为它就是255循环右移0位;而 0x000001FF 很可能是非法的)

13、b, bl, bx 指令的区别是什么?

这三种都是分支(跳转)指令,但用途不同。

指令全称区别与用途
bBranch单纯跳转。将程序计数器PC直接设置为目标地址。不保存返回地址,用于无需返回的跳转,如循环、条件分支。
blBranch with Link带链接的跳转。在跳转之前,自动将下一条指令的地址(返回地址)保存到链接寄存器LR(r14)。用于函数调用,因为函数结束后需要返回到调用处。
bxBranch and eXchange跳转并切换指令集。跳转到目标寄存器中存储的地址。根据目标地址的最低位(bit[0])来决定后续执行ARM指令(T=0)还是Thumb指令(T=1)。用于从ARM代码跳转到Thumb代码,以及函数返回(bx lr)。

简单总结

  • b跳而不返(用于goto,循环)。
  • bl跳而必返(用于调用函数)。
  • bx跳并可换(用于状态切换和间接跳转)。

14、ARM内核采用的栈是哪种栈?

  • 硬件设计:ARM内核的压栈(PUSH)操作硬件上只支持向低地址增长(递减)
  • 栈指针特性:ARM的栈指针SP(r13)指向的是栈顶最后一个被压入的有效数据(满栈)。

因此,ARM内核采用的栈是 满递减栈 (Full Descending Stack, FD)

  • 满 (Full):SP指向栈顶最后一个有效数据。
  • 递减 (Descending):栈向内存低地址方向生长。压栈时SP减小,弹栈时SP增大。

补充:四种栈类型理论模型

栈的增长方向和栈指针(SP)的位置共同定义了栈的类型,共有四种组合:

类型简称增长方向SP指向压栈(PUSH)操作弹栈(POP)操作
满递减FD向低地址最后入栈的数据先减SP,再存数据先取数据,再加SP
空递减ED向低地址下一个空位置先存数据,再减SP先加SP,再取数据
满递增FA向高地址最后入栈的数据先加SP,再存数据先取数据,再减SP
空递增EA向高地址下一个空位置先存数据,再加SP先减SP,再取数据