如何解读Keil生成的汇编列表文件?

Keil MDK-ARM生成的汇编列表文件(.lst)是C代码与ARM汇编指令的“桥梁”,能直观展示代码编译后的底层实现、指令地址、内存占用等核心信息,是代码优化、问题定位、效率分析的关键工具。本文从列表文件结构、核心板块解读、实战案例三个维度,手把手教你读懂汇编列表文件,零基础也能快速上手。

一、先明确:汇编列表文件的生成前提

解读前需确保正确生成列表文件,步骤如下(快速回顾):

  1. 打开Keil工程,点击「Options for Target」(魔法棒图标);
  2. 切换到「Listing」选项卡,勾选「Assembly Listing」;
  3. 勾选「C Compiler Listing」下的「Source」(显示C代码)、「Assembly」(显示汇编指令)、可选「Machine Code」(显示机器码);
  4. 编译工程(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的汇编列表可发现:

  1. 双层空循环的汇编指令多达20+条,每次循环需多次内存读写(STR/LDR),效率低;
  2. 循环次数7200是“经验值”,无精准的时钟周期匹配;
  3. 优化方向:改用定时器延时(减少指令数),或用寄存器操作替代内存读写。

优化对比

  • 原始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指令,导致延时失效,需在变量前加volatilevolatile uint32_t i,j;),从汇编列表可验证volatile是否生效。

场景4:定位变量内存分配问题

查看局部变量的栈分配:

  • 示例中ij是局部变量,通过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)计算。
posted @ 2025-12-15 20:46  哈希技术  阅读(1)  评论(0)    收藏  举报