Bootstrap

汇编知识点整理

链接脚本 map.lds 的解析

  1. OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")

    • 作用:指定输出文件的格式为小端存储的 32 位 ARM 架构 ELF 文件。
    • 背景:小端存储(Little-endian)指的是低字节存储在低地址,高字节存储在高地址,是 ARM 常见的存储方式。
    • 用途:确保生成的文件能被 ARM 系统识别和加载。
  2. OUTPUT_ARCH(arm)

    • 作用:明确指定输出文件的架构为 ARM。
    • 背景:ARM 架构常用于嵌入式设备。
    • 用途:确保链接生成的二进制文件能与 ARM 处理器兼容。
  3. ENTRY(_start)

    • 作用:定义程序的入口点为 _start
    • 背景:在 C 程序中,通常有 main 函数作为程序入口,而在汇编或裸机开发中,_start 通常是启动代码的位置。
    • 用途:告诉链接器程序的执行从 _start 开始。
  4. SECTIONS

    • 作用:定义内存中不同段(.text.data.bss 等)的链接规则。
    • 细节解析
      • . = 0x00000000:设置起始地址为 0x00000000
      • .text
        • ./Objects/start.o(.text):将 start.o 文件的 .text 段内容放在 .text 段的最前面。
        • *(.text):将其他文件的 .text 段内容放在后续位置。
      • .rodata
        • { *(.rodata) }:将所有文件的 .rodata 段内容放入 .rodata 段。
      • .data
        • { *(.data) }:将所有文件的 .data 段内容放入 .data 段。
      • .bss
        • 定义未初始化全局变量的起始地址为 __bss_start,结束地址为 __bss_end__

汇编的组成

汇编指令的基本格式:
    <opcode>{<cond>}{s}  <Rd>,  <Rn>,  <shifter_operand>
    解释:
    opcode:指令的功能码,用来表示当前指令的作用  
    cond:条件码,可以不写,如果写了,需要在指令执行之前先判断条件受否满足,不满足则指令不执行
    s:可以不写,如果在指令码后面加了一个s,则进行运算时运算的结果会影响到CPSR条件位
    Rd:目标寄存器
    Rn:第一操作寄存器
   shifter_operand:第二操作数   
   连起来解释:先判断条件码对应的条件是否满足,如果满足,则让第一操作寄存器和第二操作数按照指令功能码进行数据运算,运算的结果会保存到目标寄存器中,如果功能码后面加了s,结果会影响到CPSR条件位   
   
    注意:
        1.第一操作寄存器只能是寄存器,不能写数值
        2.第二操作数可以写寄存器名,也可以写一个数值,如果写数值,需要在数值前加#
        3.汇编指令不区分大小写
  1. 汇编指令

    • 数据处理指令
      • 数据搬移指令(如 mov:将数据从一个寄存器/立即数搬移到目标寄存器。
      • 位运算指令(如 andorr:用于按位操作,如清零、置位等。
      • 算术运算指令(如 addsub:完成基本加减运算。
    • 跳转指令:如 bbl,分别实现无返回和有返回跳转。
    • 内存读写指令:如 ldrstr,用于从内存中加载或存储数据。
  2. 伪操作

    • 作用:不直接生成机器码,但影响汇编器的行为。
    • 示例.text(定义代码段)、.global(声明全局符号)、.if/.else/.endif(条件编译)。
  3. 伪指令

    • 作用:虽非硬件支持的指令,但会转为实际的指令。
    • 示例ldr R0, =0x1234 会展开为多条实际指令完成数据加载。
  4. 注释

    • 单行注释:用 @;
    • 多行注释:用 /*...*/

常用汇编指令的具体解析

数据搬移指令
  1. mov{cond} Rd, Operand2

    • 作用:将操作数搬移到目标寄存器。
    • 示例mov R0, #5 将立即数 5 放入寄存器 R0
  2. mvn{cond} Rd, Operand2

    • 作用:将操作数按位取反后搬移到目标寄存器。
    • 示例mvn R1, #0x0F0xF0 放入 R1
数据移位指令
  1. 逻辑左移(LSL)
    • 格式lsl{cond} Rd, Rn, #shift
    • 示例lsl R0, R1, #2R1 的内容左移 2 位后存入 R0
  2. 循环右移(ROR)
    • 格式ror{cond} Rd, Rn, #shift
    • 示例ror R0, R1, #1
位运算指令
  1. 与运算(AND)
    • 格式and{cond} Rd, Rn, Operand2
    • 示例and R0, R1, #0xFF

  1. 或运算(ORR)
    • 格式orr{cond} Rd, Rn, Operand2
    • 示例orr R0, R1, #0x01

算术运算指令
  1. 加法(ADD)

    • 格式add{cond}{s} Rd, Rn, Operand2
    • 示例add R0, R1, R2
  2. 减法(SUB)

    • 格式sub{cond}{s} Rd, Rn, Operand2
    • 示例sub R0, R1, R2
  3. 乘法(MUL)

    • 格式mul{cond}{s} Rd, Rn, Operand2
    • 示例mul R0, R1, R2
模拟32位处理器进行64位数据运算
32位处理器一条指令最大能进行32位数据运算,想要进行64位运算,需要多条指令

第一个64位数据:
    0X00000003 FFFFFFFE
第二个64位数据:
    0X00000005 00000006
    
    
运算原则:
低32位进行运算,运算的结果影响到CPSR条件位
高32位数据进行运算,运算结果考虑到cpsr条件位

@R1 R2 保存第一个64位数据
mov R1,#0X00000003
MOV R2,#0XFFFFFFFE

@R3 R4 保存第二个64位数据
mov R3,#0X00000005
MOV R4,#0X00000006

@R5 R6保存结果
ADDS R6,R2,R4
ADC R5,R1,R3
结果:0X 00000009 00000004
比较指令

比较指令就是用来进行两个数值的比较,根据比较的结果产生不同的条件,在后面的指令如果添加了条件码后缀,则根据条件执行指令

1.cmp 第一操作寄存器,第二操作数
解释:将第一操作数和第二操作寄存器进行比较
 比较指令的本质就是比较的两个数进行减法运算,运算的结果应先到CPSR的NZCV位
 

 比较指令通过会和条件码一起使用:

1.先进行比较

2.比较后的指令后面加条件码,条件码对应的条件满足后指令才会指令

 

 2.tst 目标寄存器,#(0X1<<N)
 解释:这个指令用来判断目标寄存器的第N位是否为0
3.TEQ 目标寄存器,第二操作数
解释:判断目标寄存器的值是否和第二操作数相等
注意:TEq只能影响CPSR寄存器的z,无法影响c和v位        

 

跳转指令
  1. bbl
    • b label:无返回跳转。
    • bl label:有返回跳转,会保存返回地址到 LR
立即数
  1. ARM 汇编对立即数的限制为 0-255 内的值或其循环右移偶数位后的值。
  2. 非立即数可通过伪指令 ldr Rd, =数据 进行加载。
  3. 能够经过编码后保存到指令空间中直接当作指令一部分去执行的数据叫做立即数

  4. 判断方式
    一个32位指令空间中预留了12位空间保存当前操作数,可以通过某一个规则对操作数进行处理,将处理后的数值存放在这个12位空间中。所以处理完能够保存到12位空间中的数据就是立即数,否则不是

  5. 数据的处理规则
    将操作数循环右移偶数位 ,如果能够得到一个0-255内的数据,就说明这个数据是一个立即数
    反过来,得到的数据循环右移偶数位,也可以得到操作数
    指令中操作数的12位空间分为低8位和高4位,低8bit保存循环右移得到操作数的那个0-255内的数字
    高4位保存0-255内的数据循环右移的偶数位/2

内存读写指令

单寄存器读写
  1. 写入内存
    • str Rd, [Addr]:将 Rd 的数据存入地址 [Addr]
    • 示例str R0, [R1]
  2. 读取内存
    • ldr Rd, [Addr]:将地址 [Addr] 的数据加载到 Rd
    • 示例ldr R0, [R1]
批量寄存器读写
  1. 写入内存
    • stm{mode} Addr, {Regs}
    • 示例stmia R0!, {R1-R4}
  2. 读取内存
    • ldm{mode} Addr, {Regs}
    • 示例ldmia R0!, {R1-R4}
地址索引模式
  1. 前索引
    • ldr R0, [R1, #4]:从 R1+4 地址读取。
  2. 后索引
    • ldr R0, [R1], #4:从 R1 读取后,将 R1 加 4。
  3. 自动索引
    • ldr R0, [R1, #4]!:先将 R1 加 4,再从新地址读取。

栈的分类

根据两种标准可以将栈分为四类:

  1. 空栈和满栈

    • 空栈:压栈完毕后,SP(栈指针)保存的栈顶地址空间中没有有效数据。
    • 满栈:压栈完毕后,SP保存的栈顶地址空间中有有效数据。
  2. 增栈和减栈

    • 增栈:每次压栈一个数据,SP保存的栈顶地址往大地址增长。
    • 减栈:每次压栈一个数据,SP保存的栈顶地址往小地址增长。

两种标准两两组合形成四类栈:

  • 空增栈(EA)
  • 空减栈(ED)
  • 满增栈(FA)
  • 满减栈(FD)

ARM默认使用满减栈。

满减栈的实现

实现方式 1
  • 压栈push {寄存器列表}
  • 出栈pop {寄存器列表}

示例代码:

.text   @ 声明当前内容是一个文本段
.global _start   @ .global声明_start标签是一个全局标签

_start:
    mov sp, #0X40000020  @ 初始化栈
    b main   @ 进入main函数

main:
    mov r1, #3
    mov r2, #5
    bl fun1
    add r3, r1, r2
    b loop

fun1:
    @ 压栈保护现场
    push {r1, r2}
    mov r1, #6
    mov r2, #7
    mul r4, r1, r2
    @ 出栈恢复现场
    pop {r1, r2}
    mov pc, lr  @ 函数返回

loop:
    b loop
.end

栈内存读写示例——非叶子函数调用过程

非叶子函数需要保存调用者的返回地址(lr),并可能需要嵌套调用其他函数。

.text   @ 声明当前内容是一个文本段
.global _start   @ .global声明_start标签是一个全局标签

_start:
    mov sp, #0X40000020  @ 初始化栈
    b main   @ 进入main函数

main:
    mov r1, #3
    mov r2, #5
    bl fun1
    add r3, r1, r2
    b loop

fun1:
    @ 压栈保护现场
    push {r1, r2, lr}
    mov r1, #6
    mov r2, #7
    bl fun2
    mul r4, r1, r2
    @ 出栈恢复现场
    pop {r1, r2, pc}

fun2:
    @ 压栈保护现场
    push {r1, r2}
    mov r1, #4
    mov r2, #1
    sub r5, r1, r2
    @ 出栈恢复现场
    pop {r1, r2}
    mov pc, lr

loop:
    b loop
.end

状态寄存器传送指令

ARM处理器的状态寄存器CPSR保存了程序当前的工作状态。可以通过状态寄存器传送指令对其进行读取和修改。

读状态寄存器
MRS 目标寄存器, CPSR
  • CPSR寄存器数值读取并保存到目标寄存器。
修改状态寄存器
MSR CPSR, 操作数
  • 将操作数写入CPSR寄存器。

注意

  • 在特权模式下可以通过修改CPSR切换到用户模式。
  • 在用户模式下无法通过修改CPSR切换到特权模式。

软中断产生指令

通过软中断指令可以让处理器进入SVC模式处理软中断。

指令格式
swi 操作数
  • 操作数是一个立即数,表示产生的软中断的中断号。
示例代码
.text   @ 声明当前内容是一个文本段
.global _start   @ .global声明_start标签是一个全局标签

_start:
    b reset
    b .
    b do_swi
    b . 
    b .
    b .
    b .
    b .

reset:
    mov sp, #0X40000020  @ 初始化SVC模式下的栈
    MSR CPSR, #0x10      @ 切换到USER模式执行
    mov sp, #0X40000020  @ 初始化USER模式下的栈
    b main

main:
    mov r1, #1
    mov r2, #6
    swi 1                @ 软中断产生
    add r3, r1, r2
    b loop

do_swi:
    push {r1, r2, lr}    @ 压栈保护现场
    mov r1, #5
    mov r2, #8
    mul r4, r1, r2
    ldmfd sp!, {r1, r2, pc}^  @ 出栈并恢复异常

loop:
    b loop
.end

混合编程

混合编程指C语言和汇编语言之间的相互调用。

1. 汇编调用C语言函数

汇编可以将C语言函数当作标签直接调用。

// C文件
int add_func(int a, int b, int c, int d) {
    return (a + b + c + d);
}
// 汇编文件
.text
.global _start

_start:
    ldr sp, =0x40000820  @ 初始化栈指针
    mov r0, #3           @ 参数1
    mov r1, #4           @ 参数2
    mov r2, #5           @ 参数3
    mov r3, #6           @ 参数4
    bl add_func          @ 调用C函数

loop:
    b loop
.end
2. C语言调用汇编标签

C语言可以通过extern声明调用汇编中的函数。

// C文件
extern int add_func(int a, int b, int c, int d);

int main() {
    int sum = add_func(1, 2, 3, 4);
    while (1);
    return 0;
}
// 汇编文件
.text
.global add_func

add_func:
    add r0, r0, r1
    add r0, r0, r2
    add r0, r0, r3
    mov pc, lr
.end

C语言内联汇编

通过asm关键字,可以在C代码中直接嵌套汇编代码。

格式
asm volatile (
    "汇编指令\n\t"
    : 输出操作数列表
    : 输入操作数列表
    : 破坏描述符
);
示例
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;
}
;