一:ARM 可执行程序的生成过程
1. 课程内容介绍
- 汇编语言:汇编语言是与计算机硬件直接交互的低级语言,使用助记符表示机器指令。学习汇编语言有助于理解计算机的工作原理和优化程序性能。
- 调试程序:调试是软件开发中不可或缺的一部分,调试程序帮助开发者识别和修复代码中的错误。掌握调试工具(如 GDB)是提高开发效率的关键。
- C 和 C++ 逆向工程:逆向工程是分析和理解已有程序的过程,通常用于安全分析、漏洞挖掘和软件兼容性研究。学习 C 和 C++ 的逆向工程有助于理解程序的结构和逻辑。
2. ARM 汇编与指令集
- ARM 指令集:ARM 是一种广泛使用的 RISC(精简指令集计算)架构,具有高效的指令集和低功耗特性。学习 ARM 指令集有助于编写高效的嵌入式系统和移动应用程序。
- ARM 可执行程序的生成过程:了解 ARM 可执行程序的生成过程,包括编写源代码、编译、链接等步骤,是掌握 ARM 开发的基础。
ARM 可执行程序的生成步骤
1. 通过例子学习生成过程
- 通过一个简单的示例(如 Hello World 程序),逐步演示 ARM 可执行程序的生成过程,帮助学生理解每个步骤的作用。
2. 步骤包括编写源代码、编译、链接等
- 编写源代码:使用 C 或 C++ 编写源代码,定义程序的功能。
- 编译:将源代码转换为机器代码,生成中间文件。
- 链接:将多个中间文件和库文件链接成最终的可执行文件。
编写 Hello World 程序
1. 编写最简单的 Hello World 程序
-
示例代码:
#include <stdio.h> int main() { printf("Hello, World!\\n"); return 0; }
2. 使用 clang 编译器进行编译
-
使用 clang 编译器生成 64 位可执行文件:
clang -o hello hello.c
编译 32 位 ARM 可执行程序
1. 修改编译器目标架构
-
使用
target
选项指定目标架构为 32 位 ARM:clang -target armv7-none-linux-gnueabi -o hello32 hello.c
2. 设置环境变量和使用 NDK 中的 clang
- 介绍如何配置 Android NDK 环境,确保可以使用 NDK 中的 clang 编译器。
生成过程详细步骤
1. 预编译
- 生成
.i
文件,展开宏和头文件,便于后续编译。
2. 编译
- 将预编译文件编译成汇编文件
.s
,生成可读的汇编代码。
3. 汇编
- 将汇编文件汇编成中间文件
.o
,这是机器代码的中间表示。
4. 链接
- 将多个
.o
文件链接成最终的可执行程序,生成可执行文件。
二.ARM 汇编语言结构
1. 汇编语言的结构
- 介绍 ARM 汇编语言的基本结构,包括:
- 处理器指定:指定使用的 ARM 处理器类型。
- 段定义:定义代码段和数据段。
- 符号表:用于管理变量和函数的地址。
汇编语言的结构是理解汇编程序编写和执行的基础。以下是汇编语言的一些基本组成部分和结构要素:
1. 源代码文件
汇编语言的源代码通常以 .asm
或 .s
为文件扩展名。文件中包含了汇编指令、数据定义和注释。
2. 段定义
汇编程序通常分为几个段,每个段用于不同的目的。常见的段包括:
- 代码段(.text):存放程序的可执行指令。
- 数据段(.data):存放已初始化的全局变量和静态变量。
- BSS 段(.bss):存放未初始化的全局变量和静态变量。
- 堆栈段(.stack):用于存放函数调用时的局部变量和返回地址。
示例:
.section .text
.global _start
_start:
; 程序入口
3. 指令
指令是汇编语言的核心部分,指示 CPU 执行特定操作。指令通常由操作码(opcode)和操作数(operand)组成。
- 操作码:指示要执行的操作类型(如加法、减法、数据传送等)。
- 操作数:指示操作的对象,可以是寄存器、内存地址或立即数。
示例:
MOV R0, #5 ; 将立即数 5 移动到寄存器 R0
ADD R1, R0, #3 ; 将 R0 和立即数 3 相加,结果存入 R1
4. 标签
标签是程序中的标识符,用于标记特定位置,通常用于控制流(如跳转和循环)。标签后面跟一个冒号。
示例:
loop_start:
; 循环体
B loop_start ; 跳转到 loop_start 标签
5. 注释
注释用于解释代码,帮助程序员理解程序的逻辑。注释通常以 ;
开头,后面的内容将被汇编器忽略。
示例:
MOV R0, #5 ; 将 5 移动到 R0 寄存器
6. 伪指令
伪指令是汇编语言中的指令,虽然不直接对应于机器指令,但用于控制汇编过程或定义数据。常见的伪指令包括:
- .data:定义数据段。
- .text:定义代码段。
- .global:声明全局符号。
- .equ:定义常量。
示例:
.data
myVar: .word 10 ; 定义一个名为 myVar 的变量,初始值为 10
7. 符号表
符号表用于存储程序中使用的变量、函数和标签的名称及其地址。汇编器在汇编过程中使用符号表来解析标签和变量的地址。
8. 汇编指令的格式
汇编指令的格式通常为:
[标签] 指令 [操作数]
其中,标签和操作数是可选的。
示例汇编程序
以下是一个简单的汇编程序示例,展示了上述结构的应用:
.section .data
msg: .asciz "Hello, World!\\n" ; 定义字符串
.section .text
.global _start
_start:
; 写入字符串到标准输出
MOV R0, #1 ; 文件描述符 1 (stdout)
LDR R1, =msg ; 加载字符串地址
MOV R2, #14 ; 字符串长度
SWI 0x9000004 ; 系统调用:write
; 退出程序
MOV R0, #0 ; 退出状态
SWI 0x9000001 ; 系统调用:exit
2.语法、指令和伪指令
讲解汇编语言中的基本语法规则、常用指令和伪指令的使用。
1. 基本语法规则
汇编语言的语法规则通常包括以下几个方面:
1.1 指令格式
汇编指令的基本格式为:
[标签] 指令 [操作数]
- 标签(可选):用于标识指令的位置,通常以冒号结尾。
- 指令:表示要执行的操作。
- 操作数(可选):指令的操作对象,可以是寄存器、内存地址或立即数。
1.2 大小写
汇编语言通常不区分大小写,但为了可读性,指令和伪指令通常使用大写字母,而标签和变量名使用小写字母。
1.3 注释
注释以 ;
开头,后面的内容将被汇编器忽略。注释用于解释代码,帮助程序员理解程序逻辑。
示例:
MOV R0, #5 ; 将 5 移动到 R0 寄存器
1.4 空格和缩进
指令和操作数之间用空格或制表符分隔。标签和指令之间可以使用空格或制表符进行缩进,以提高可读性。
3. 常用指令
汇编语言中的指令通常分为几类,以下是一些常用的指令及其功能:
3.1 数据传送指令
-
MOV:将数据从一个位置移动到另一个位置。
MOV R0, R1 ; 将 R1 的值移动到 R0 MOV R2, #10 ; 将立即数 10 移动到 R2
3.2 算术运算指令
-
ADD:执行加法运算。
ADD R0, R1, R2 ; 将 R1 和 R2 相加,结果存入 R0
-
SUB:执行减法运算。
SUB R0, R1, R2 ; 将 R1 减去 R2,结果存入 R0
3.3 逻辑运算指令
-
AND:执行按位与运算。
AND R0, R1, R2 ; R1 和 R2 按位与,结果存入 R0
-
ORR:执行按位或运算。
ORR R0, R1, R2 ; R1 和 R2 按位或,结果存入 R0
3.4 控制流指令
-
B:无条件跳转。
B label ; 跳转到指定标签
-
BL:分支并链接,用于调用函数。
BL function ; 调用 function 函数
-
CMP:比较两个寄存器的值。
CMP R0, R1 ; 比较 R0 和 R1
3.5 条件跳转指令
-
BEQ:如果相等则跳转。
BEQ label ; 如果上一次比较结果相等,则跳转到 label
-
BNE:如果不相等则跳转。
BNE label ; 如果上一次比较结果不相等,则跳转到 label
4. 伪指令
伪指令是汇编语言中的指令,虽然不直接对应于机器指令,但用于控制汇编过程或定义数据。常见的伪指令包括:
4.1 段定义伪指令
-
.data:定义数据段,存放已初始化的变量。
.data myVar: .word 10 ; 定义一个名为 myVar 的变量,初始值为 10
-
.text:定义代码段,存放可执行指令。
.text
4.2 全局符号伪指令
-
.global:声明全局符号,使其在其他文件中可见。
.global _start ; 声明 _start 为全局符号
4.3 常量定义伪指令
-
.equ:定义常量。
PI: .equ 3.14 ; 定义常量 PI
4.4 字节和字定义伪指令
-
.byte:定义一个字节。
myByte: .byte 0xFF ; 定义一个字节,值为 255
-
.word:定义一个字(通常为 4 字节)。
myWord: .word 0x12345678 ; 定义一个字,值为 0x12345678
三:ARM 寄存器介绍
1. ARM32 位架构中的寄存器
- 介绍 ARM32 位架构中的寄存器类型:
- 通用寄存器:用于存储数据和地址。
- 分组寄存器:用于特定功能,如程序计数器(PC)和堆栈指针(SP)。
- 特殊寄存器:用于控制和状态信息。
2. 各寄存器的用途和含义
在 ARM 架构中,寄存器是 CPU 内部用于存储数据和地址的高速存储单元。ARM 处理器通常有多种类型的寄存器,每种寄存器都有特定的功能和用途。以下是 ARM 架构中各类寄存器的详细解释:
1. 通用寄存器(R0 - R15)
ARM 处理器通常有 16 个通用寄存器,编号从 R0 到 R15。它们的主要功能如下:
- R0 - R12:这些寄存器是通用寄存器,可以用于存储数据、地址或临时变量。程序员可以自由使用这些寄存器进行计算和数据传输。
- R13(SP):堆栈指针寄存器(Stack Pointer)。它指向当前堆栈的顶部,堆栈用于存储函数调用时的局部变量和返回地址。堆栈的操作通常涉及到 PUSH 和 POP 指令。
- R14(LR):链接寄存器(Link Register)。在调用函数时,返回地址会被存储在 LR 中。当函数执行完毕后,可以通过 BX LR 指令返回到调用函数的位置。
- R15(PC):程序计数器(Program Counter)。它指向当前正在执行的指令的地址。每当执行一条指令时,PC 会自动增加,以指向下一条指令。PC 也可以被直接修改,以实现跳转和分支。
2. 特殊寄存器
除了通用寄存器,ARM 还包含一些特殊寄存器,用于特定的功能:
- CPSR(Current Program Status Register):当前程序状态寄存器。它包含了当前处理器的状态信息,包括条件标志(如零标志、负标志、溢出标志和进位标志)、中断使能位和当前处理器模式(如用户模式、特权模式等)。CPSR 的状态会影响条件执行指令的行为。
- SPSR(Saved Program Status Register):保存程序状态寄存器。它用于在处理异常或中断时保存 CPSR 的状态,以便在处理完后恢复。SPSR 主要在异常处理程序中使用。
3. 浮点寄存器(VFP 寄存器)
在支持浮点运算的 ARM 处理器中,通常有一组浮点寄存器(V0 - V31),用于存储浮点数和进行浮点运算。这些寄存器的使用场景包括:
- 浮点计算:执行浮点加法、减法、乘法和除法等运算。
- SIMD 操作:在某些 ARM 处理器中,浮点寄存器还可以用于单指令多数据(SIMD)操作,以提高数据处理效率。
4. SIMD 寄存器(NEON 寄存器)
在支持 NEON 技术的 ARM 处理器中,通常有一组 SIMD 寄存器(Q0 - Q31),用于并行处理多个数据元素。这些寄存器的使用场景包括:
- 图像处理:在图像处理应用中,NEON 寄存器可以用于并行处理像素数据。
- 音频处理:在音频信号处理应用中,NEON 寄存器可以用于并行处理音频样本。
5. 寄存器的使用场景
- 函数参数传递:在 ARM 架构中,通常使用 R0 - R3 寄存器传递前四个函数参数,超出部分则通过堆栈传递。
- 返回值:函数的返回值通常存储在 R0 中。
- 临时存储:在执行复杂计算时,可以使用 R4 - R12 作为临时存储寄存器。
- 异常处理:在处理异常时,CPSR 和 SPSR 寄存器用于保存和恢复处理器状态。
总结
ARM 架构中的寄存器分为通用寄存器和特殊寄存器,每种寄存器都有特定的功能和使用场景。理解各寄存器的用途和含义对于编写高效的汇编程序和优化代码性能至关重要。通过合理使用寄存器,程序员可以提高程序的执行效率,减少内存访问的开销。
四:汇编指令详解
1. 常用指令
- 详细解释汇编语言中的常用指令,如:
- PUSH:将数据压入堆栈。
- MOVE:数据传送指令。
- STR:将寄存器中的数据存储到内存。
- LDR:从内存加载数据到寄存器。
- ADD:加法运算指令。
- BL:分支并链接,用于调用函数。
2. 实例说明指令的作用和用法
参考我之前发的文章。