如何解读Keil生成的汇编列表文件?
Keil MDK-ARM生成的汇编列表文件(.lst)是C代码与ARM汇编指令的“桥梁”,能直观展示代码编译后的底层实现、指令地址、内存占用等核心信息,是代码优化、问题定位、效率分析的关键工具。本文从列表文件结构、核心板块解读、实战案例三个维度,手把手教你读懂汇编列表文件,零基础也能快速上手。
一、先明确:汇编列表文件的生成前提
解读前需确保正确生成列表文件,步骤如下(快速回顾):
- 打开Keil工程,点击「Options for Target」(魔法棒图标);
- 切换到「Listing」选项卡,勾选「Assembly Listing」;
- 勾选「C Compiler Listing」下的「Source」(显示C代码)、「Assembly」(显示汇编指令)、可选「Machine Code」(显示机器码);
- 编译工程(Build/Rebuild),在指定「Output Path」下生成
.lst文件(如main.lst)。
二、汇编列表文件的核心结构
以STM32F103工程的main.lst为例,文件整体分为4个核心板块,按顺序解读:
板块1:文件头与编译信息(基础上下文)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Keil ARM Compiler V5.06 update 7 (build 960)
# Command line: --cpu Cortex-M3.fp -D__UVISION_VERSION="536" -I./Inc -O0 -DUSE_STDPERIPH_DRIVER -c -o main.o main.c
# Input file: main.c
# Output file: main.o
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
解读要点:
- 编译器版本、编译指令(如
--cpu Cortex-M3.fp指定内核、-O0无优化); - 输入/输出文件路径(确认列表文件对应正确的C文件);
- 编译宏定义(如
USE_STDPERIPH_DRIVER),辅助理解代码编译条件。
板块2:C代码与汇编指令的逐行对应(核心)
这是列表文件最关键的部分,C代码行与汇编指令一一映射,示例如下:
; *** main.c ***
1 #include "stm32f10x.h"
2
3 void delay_ms(uint32_t ms)
4 {
5 uint32_t i,j;
6 for(i=0; i<ms; i++)
7 for(j=0; j<7200; j++);
8 }
9
10 int main(void)
11 {
12 GPIO_InitTypeDef GPIO_InitStructure;
13 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
14
15 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
16 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
17 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
18 GPIO_Init(GPIOA, &GPIO_InitStructure);
19
20 while(1)
21 {
22 GPIO_SetBits(GPIOA, GPIO_Pin_0);
23 delay_ms(1000);
24 GPIO_ResetBits(GPIOA, GPIO_Pin_0);
25 delay_ms(1000);
26 }
27 }
; 编译后的汇编指令(delay_ms函数)
delay_ms:
0x08000080 B580 PUSH {r7,lr} ; 保存r7、lr寄存器(函数调用栈)
0x08000082 B081 SUB sp,sp,#0x04 ; 栈指针减4,分配局部变量空间
0x08000084 AF00 ADD r7,sp,#0x00 ; r7指向栈帧起始地址
0x08000086 6078 STR r0,[r7,#0x00] ; 将ms参数(r0)存入栈(i的地址)
0x08000088 2000 MOVS r0,#0x00 ; r0=0(i的初始值)
0x0800008A 6038 STR r0,[r7,#0x04] ; 将i=0存入栈
L0001:
0x0800008C 687B LDR r3,[r7,#0x00] ; 读取ms值到r3
0x0800008E 683A LDR r2,[r7,#0x04] ; 读取i值到r2
0x08000090 429A CMP r2,r3 ; 比较i和ms(i<ms?)
0x08000092 D20A BCC L0002 ; 若i<ms,跳转到L0002
0x08000094 3704 SUBS r7,#0x04 ; 恢复r7
0x08000096 46BD MOV sp,r7 ; 恢复栈指针
0x08000098 BD80 POP {r7,pc} ; 恢复r7,返回(pc=lr)
L0002:
0x0800009A 2000 MOVS r0,#0x00 ; r0=0(j的初始值)
0x0800009C 60F8 STR r0,[r7,#0x08] ; 将j=0存入栈
L0003:
0x0800009E 68FA LDR r2,[r7,#0x08] ; 读取j值到r2
0x080000A0 F44F7200 MOV.W r2,#0x1C20 ; r2=7200(十进制)
0x080000A4 429A CMP r2,r3 ; 比较j和7200(j<7200?)
0x080000A6 D206 BCC L0004 ; 若j<7200,跳转到L0004
0x080000A8 683B LDR r3,[r7,#0x04] ; 读取i值到r3
0x080000AA 3301 ADDS r3,#0x01 ; i=i+1
0x080000AC 603B STR r3,[r7,#0x04] ; 保存i到栈
0x080000AE E7F6 B L0001 ; 跳回L0001,继续外层循环
L0004:
0x080000B0 68FB LDR r3,[r7,#0x08] ; 读取j值到r3
0x080000B2 3301 ADDS r3,#0x01 ; j=j+1
0x080000B4 60FB STR r3,[r7,#0x08] ; 保存j到栈
0x080000B6 E7F8 B L0003 ; 跳回L0003,继续内层循环
; main函数汇编片段(GPIO初始化)
main:
0x08000100 B580 PUSH {r7,lr}
0x08000102 B082 SUB sp,sp,#0x08
0x08000104 AF00 ADD r7,sp,#0x00
0x08000106 2104 MOVS r1,#0x04 ; RCC_APB2Periph_GPIOA=0x04
0x08000108 2001 MOVS r0,#0x01 ; ENABLE=1
0x0800010A F7FFFFFE BL.W RCC_APB2PeriphClockCmd ; 调用时钟使能函数
...
0x08000120 F7FFFFFE BL.W GPIO_Init ; 调用GPIO_Init函数
...
0x08000130 F7FFFFFE BL.W delay_ms ; 调用delay_ms(1000)
核心列解读:
| 列名 | 示例值 | 含义 |
|---|---|---|
| 地址列 | 0x08000080 | 指令在Flash中的存储地址(STM32F103默认Flash起始地址0x08000000) |
| 机器码列 | B580 | 指令对应的16进制机器码(可用于反汇编/烧录验证) |
| 汇编指令列 | PUSH | ARM汇编指令(对应C代码的底层操作) |
| 注释列(;后) | 保存r7、lr寄存器 | 指令的功能解释(Keil自动生成,或手动添加) |
关键汇编指令速查(Cortex-M3):
PUSH/POP:入栈/出栈(函数调用时保存/恢复寄存器);STR/LDR:将数据存入内存/从内存读取数据(操作局部变量/参数);MOVS/CMP:赋值/比较(实现C语言的赋值、条件判断);B/BL:无条件跳转/带返回跳转(实现循环、函数调用);SUB/ADD:栈指针操作(分配/释放局部变量空间)。
板块3:符号与地址映射(辅助定位)
列表文件末尾会标注函数、变量的符号地址,示例:
Symbols in module:
delay_ms 0x08000080 Function 0x00000036
main 0x08000100 Function 0x00000050
i 0x20000000 Local 0x00000004
j 0x20000004 Local 0x00000004
解读:
Function:函数符号,值为函数起始地址,Size为函数占用Flash大小;Local:局部变量,值为变量在SRAM中的地址,Size为变量占用字节数;- 可快速定位函数/变量的内存位置,辅助调试。
板块4:编译统计信息(资源分析)
Compilation statistics:
Code size: 0x00000086 (134) bytes
RO Data size: 0x00000000 (0) bytes
RW Data size: 0x00000004 (4) bytes
ZI Data size: 0x00000008 (8) bytes
解读:
Code size:该C文件编译后的代码占用Flash大小;RO Data:只读常量占用Flash大小;RW Data:初始化全局变量占用SRAM大小;ZI Data:未初始化全局变量占用SRAM大小;- 用于评估单个文件的内存占用,辅助资源优化。
三、实战:汇编列表文件的典型解读场景
场景1:分析函数执行效率(优化延时函数)
从delay_ms的汇编列表可发现:
- 双层空循环的汇编指令多达20+条,每次循环需多次内存读写(
STR/LDR),效率低; - 循环次数7200是“经验值”,无精准的时钟周期匹配;
- 优化方向:改用定时器延时(减少指令数),或用寄存器操作替代内存读写。
优化对比:
- 原始
delay_ms:占用Flash 0x36字节,循环依赖内存读写; - 定时器延时函数:仅需配置定时器寄存器(10+条指令),占用Flash仅0x10字节,且延时精准。
场景2:定位代码执行异常(函数调用栈错误)
若程序运行时死机,查看汇编列表中函数的栈操作:
- 检查
PUSH/POP寄存器数量是否匹配(如PUSH {r7,lr}需对应POP {r7,pc}); - 检查栈指针(SP)操作是否合理(如
SUB sp,sp,#0x04分配空间后,是否有ADD sp,sp,#0x04恢复); - 示例错误:函数返回时未恢复SP,导致栈溢出,可从汇编列表中直接定位。
场景3:验证编译器优化效果
编译时调整优化等级(如-O1/-O2),对比汇编列表:
-O0(无优化):汇编指令与C代码逐行对应,冗余指令多(便于调试);-O2(高优化):编译器会合并指令、删除冗余操作(如空循环可能被优化掉);- 示例:
delay_ms的空循环在-O2下会被编译为一条NOP指令,导致延时失效,需在变量前加volatile(volatile uint32_t i,j;),从汇编列表可验证volatile是否生效。
场景4:定位变量内存分配问题
查看局部变量的栈分配:
- 示例中
i和j是局部变量,通过SUB sp,sp,#0x04分配栈空间,地址为[r7,#0x00]/[r7,#0x04]; - 若局部变量过多(如数组),栈空间不足会导致溢出,从汇编列表的
SP操作可计算栈占用大小,提前规避。
四、解读技巧与避坑指南
1. 关键技巧
- 关联C代码行号:列表文件中C代码行号与汇编指令一一对应,重点关注循环、函数调用、条件判断的汇编实现;
- 熟悉ARM Cortex-M指令集:重点掌握
PUSH/POP(栈操作)、STR/LDR(内存操作)、BL(函数调用)、BCC/BEQ(条件跳转); - 结合链接列表文件(.m51):汇编列表看单个文件的指令,链接列表看全局内存分布,两者结合更全面;
- 调试时对比反汇编窗口:Keil调试模式下打开「Disassembly Window」,可将列表文件的汇编指令与实时执行的指令对比,定位执行异常。
2. 常见坑点
- 优化等级导致汇编指令差异:不同优化等级(-O0/-O1/-O2)的汇编指令差异极大,解读前确认编译优化等级;
- 宏展开未显示:若C代码包含大量宏,需在Listing配置中勾选
-ap(显示宏展开),否则汇编指令与宏定义无法对应; - 机器码与指令不匹配:部分指令(如32位
MOV.W)会占用2个16进制机器码,需注意地址递增步长(如0x080000A0到0x080000A4,步长4); - 局部变量地址偏移:
r7是栈帧指针,局部变量地址为[r7,#偏移量],偏移量需结合栈分配指令(SUB sp,sp,#0x04)计算。

浙公网安备 33010602011771号