0.序言

以UCOS II为例,我要把它移植到mini2440。

在VScode中打开一个elf文件,一般是不能直接查看的。

点击中间的仍然打开,可以在上方选择十六进制编辑器。

进入显示了16进制的页面,那么我们面对依然比较难解读的代码如何解读呢。一般我们采用readelf命令进行解读。

1.elf header

在WSL terminal中使用命令,cmd或者powershell用这个没有用,以我make出来的mini2440_rtos.elf文件举例。

在vscode terminal中点击+号下拉菜单选择WSL

输入命令,注意要先cd到你的elf文件目录

readelf -h mini2440_rtos.elf

终端显示结果

ELF 文件头位于文件偏移 0x000x34 (52字节) 处,定义了文件的基本属性与解释方式。

1.1 标识与架构 (Identification & Machine)

  • Magic Number: 7f 45 4c 46 ...,确认为标准的 ELF 格式文件。

  • Class: ELF32。表明该系统采用 32 位地址空间,指针与长整型宽度为 4 字节,适配 ARM9 32位 RISC 架构。

  • Data: 2's complement, little endian。数据采用小端序存储(低字节在低地址),这与 Makefile 中未指定大端参数(默认小端)的行为一致,且符合 S3C2440 通常的运行模式。

  • Machine: ARM。指定目标指令集架构为 ARM。

  • Flags: 0x5000200, Version5 EABI, soft-float ABI

    • Version5 EABI: 遵循 ARM 嵌入式应用二进制接口 (EABI) 第 5 版标准,规范了函数调用约定(如寄存器传参 R0-R3)、堆栈对齐(8字节对齐)等。

    • soft-float ABI: 表明二进制代码不使用硬件浮点指令(ARM920T 无 FPU),所有浮点运算均通过软件库函数(如 __aeabi_fadd)模拟实现。

1.2 文件类型与入口 (Type & Entry)

  • Type: EXEC (Executable file)。表明文件已完成重定位(Relocation)与符号解析,包含绝对虚拟地址,可直接加载运行。

  • Entry point address: 0x30000000

    • 物理意义: 这是 CPU 获取控制权后执行的第一条指令地址。

    • 构建溯源: 链接脚本 link.ld 通过 ENTRY(_start) 指令指定入口符号,而 .text 段被脚本强制定位在 0x30000000。汇编源码 startup.S_start 标号置于代码段之首,从而确立了该入口地址。

startup.s中的_start入口

2.Section Headers

如果 Header 是封面,Section Headers 就是目录。它告诉我们要去哪里找代码、找数据。

运行命令:

arm-none-eabi-readelf -S build/mini2440_rtos.elf

可以看到有15个Section。这些是编译器归类放置东西的区域划分。

2.1 代码段 (.text)

  • 范围: 0x30000000 - 0x3000800d

  • 大小: 0x00800d

  • 属性: AX (Alloc, Execute)。

  • 内容: 包含异常向量表、启动代码、操作系统内核代码及用户应用代码。

  • 对齐现象: 代码段结束于非 4 字节对齐地址 0x3000800d

2.2 填充 (Padding)

  • 现象: 下一节区 .data 起始于 0x30008010

  • 分析: 存在 3 字节 (0x30008010 - 0x3000800d = 3) 的空隙。

  • 成因: link.ld 脚本在 .data 段定义前使用了 . = ALIGN(4); 指令。这是为了确保数据段起始地址满足 ARM 处理器的 32 位访问对齐要求,防止非对齐访问异常(Data Abort)。

2.3 已初始化数据段 (.data)

  • 范围: 0x30008010 - 0x30008118

  • 大小: 0x000108 (264 Bytes)。

  • 属性: WA (Write, Alloc)。

  • 内容: 包含代码中显式赋值的全局变量,如 os_core.c 中的查找表。

2.4 未初始化数据段 (.bss)

  • 范围: 0x30008118 - 0x3000b8e8

  • 大小: 0x0037d0 (14,288 Bytes)。

  • 属性: WA (Write, Alloc), NOBITS

  • 验证: 结束地址 0x30008118 + 0x37d0 = 0x3000b8e8,计算无误。此区域包含大型数据结构,如任务堆栈数组。

2.5 调试信息段 (.debug_*)

  • 特征: 地址均为 00000000,不占用目标机内存。

  • debug_line: 映射机器码地址与 C 源码行号。

  • debug_info: 存储 DWARF 格式的类型信息与变量作用域。

  • debug_frame: 存储调用栈帧信息(CFI),用于调试器回溯函数调用栈。

3. Program Headers

程序头表描述了系统加载器(Loader)如何将文件映射到物理内存。本镜像仅包含一个 LOAD 类型的段(Segment),表明其为单一连续的内存镜像。

3.1 静态加载地址映射

  • VirtAddr (虚拟地址): 0x30000000

  • PhysAddr (物理地址): 0x30000000

  • 分析: 虚拟地址与物理地址恒等,表明该程序被设计为直接在 SDRAM 控制器映射的基地址(S3C2440 的 Bank 6起始地址)处运行,不开启 MMU 地址转换,或 MMU 采用平坦映射(Flat Mapping)。

3.2 文件镜像与内存镜像的差异

  • FileSiz (文件占用): 0x08118 (33,048 Bytes)。包含代码指令与已初始化的全局变量。

  • MemSiz (内存占用): 0x0b8e8 (47,336 Bytes)。包含代码、已初始化数据及未初始化数据(BSS)。

  • BSS 空间计算: MemSiz - FileSiz = 0x37D0 (14,288 Bytes)。

    • 该差值精确对应了程序运行时所需的未初始化数据区大小。系统启动时,必须由启动代码(Startup Code)将这 0x37D0 字节的内存区域清零,否则未初始化变量将含有随机值。

CPU 并不关心你有多少个小抽屉(Section),它只关心内存块(Segment)。加载器会把属性相似的 Section 合并成一个 Segment (段)。 命令:readelf -l 

让我们看看 mini2440_rtos.elf 的 Program Headers:

  • Type: LOAD: 这表示这个段必须被加载到内存里。

  • VirtAddr (虚拟地址/运行地址 VMA): 代码运行时应该在的地方。对于 mini2440,是在 SDRAM (0x30000000)。

  • PhysAddr (物理地址/加载地址 LMA): 代码如果不运行,静静躺着时应该在的地方。

    • 注意:在裸机开发中,如果你的代码在 NOR Flash 运行,VMA 可能等于 LMA。但如果你的代码要从 NAND 拷贝到 SDRAM 运行,Linker Script 里就要仔细处理这两个地址的差异。你的 mini2440_rtos 似乎是直接链接到 SDRAM 地址,这意味着你可能通过 J-Link 直接把程序写到了 SDRAM,或者 Bootloader 帮你完成了搬运。

  • Flg (Flags):

    • R E (Read, Execute): 这是代码段。只读,可执行。对应 .text

    • RW (Read, Write): 这是数据段。可读写。对应 .data.bss

关键点:操作系统加载程序时,根本不看 Section Headers,只看 Program Headers。它按照这里的指示,把文件切块,“映射”到内存里。

4. Symbol Table(符号表)

符号表提供了内存地址与源码变量/函数的直接映射关系。可以用 arm-none-eabi-nmreadelf -s 查看。

arm-none-eabi-objdump -s mini2440_rtos.elf > 1.txt

4.1 启动与异常处理

  • _start (0x30000000): 物理入口。对应 startup.S 中的异常向量表基址。

    • 向量表布局:

      • 0x00: b reset

      • 0x04: b undef

      • ...

  • reset (0x30000110): 复位异常处理程序实体入口。

    • 偏移分析: 从 0x300000000x30000110 之间的 0x110 (272) 字节空间,包含了 8 条跳转指令(32字节)以及其后的 LED 错误指示处理代码(undef, swi, pabort 等处理程序)。这表明复位代码被安排在其他异常处理桩代码之后。

4.2 链接脚本边界符号

以下符号由 link.ld 自动生成,用于运行时内存管理:

  • _etext / _data (0x30008010): 准确标记了只读代码区与可读写数据区的分界线。

  • _bss_start_ (0x30008118): 标记清零区的起始。

  • _bss_end_ (0x3000b8e8): 标记清零区的结束。

4.3 关键变量布局

  • StartStk (0x30008118):

    • 定义: main.c 中定义的 OS_STK StartStk[...]

    • 位置: 位于 .bss 段的绝对首地址

    • 风险评估: 由于 StartStk 紧邻 .data 段(0x30008118 之前即为 .data),若该栈发生向下溢出 (Stack Overflow),将直接破坏 .data 段末尾的全局变量,导致系统出现难以调试的逻辑错误。这是内存布局上的一个高风险点。

  • OSCPUUsage (0x3000ac0d):

    • 大小: 1 字节 (OBJECT 类型)。

    • 位置: 位于 .bss 段深处,表明其为未初始化的统计变量。

4.4 绝对地址符号 (ABS)

  • BWSCON (0x48000000), GPFCON (0x56000050) 等。

  • 来源: 这些符号源自 2440addr.inc 等头文件中的宏定义 (.equ)。

  • 性质: 它们不代表内存中的变量,而是直接指向 S3C2440 处理器的特殊功能寄存器 (SFR) 物理地址,用于外设控制。

5.调试信息的迷宫(DWARF)

ELF 文件里有几个巨大的段:.debug_info, .debug_abbrev, .debug_str。它们使用的是 DWARF 格式(Debugging With Attributed Record Formats)。

它像是一棵树。

  • 编译单元 (CU): 根节点。比如 main.c

  • 子节点: 函数 main

    • 属性:起始 PC 0x30000458,结束 PC 0x3000047c

    • 属性:源文件行号。

  • 子节点: 变量 key

    • 属性:类型 int (4字节)。

    • 属性:位置 fp - 16 (栈帧指针减16的位置)。

当你用 GDB 调试时输入 print key

  1. GDB 查 DWARF 表,找到 key 的定义。

  2. GDB 发现它是 int,且在栈上。

  3. GDB 读取寄存器 fp 的值,减去 16,得到内存地址。

  4. GDB 读取那个内存地址的 4 个字节,按整数解释,显示给你。

如果没有这些段(比如你用了 strip 命令或者没加 -g),GDB 就只会对着一堆寄存器和十六进制发呆。

6.总结

mini2440_rtos.elf 的全处理流程如下:

  1. 预处理 (Pre-processing): main.c 被处理,头文件展开。

  2. 汇编 (Compilation): 编译器将其变成汇编,然后变成 .o (Relocatable ELF)。此时全是碎片,地址未定。

  3. 链接 (Linking): ldmain.o, os_core.o, startup.o 等聚在一起。

    1. 根据 link.ld 脚本,决定大家住哪(0x30000000)。

    2. 解决所有的符号引用。

    3. 生成 Program Headers,规划内存布局。

    4. 此刻,mini2440_rtos.elf 诞生了。

  4. 加载 (Loading):

    1. 你通过 J-Link 点击“Download”。

    2. J-Link 软件读取 ELF Header,发现是 ARM 程序。

    3. 读取 Program Headers,看到有一个 LOAD 段要放在 0x30000000

    4. 它把文件里对应位置的二进制数据通过 JTAG 协议烧写到板子的 SDRAM 中。

    5. 最后,它读取 ELF Header 的 Entry Point,把 CPU 的 PC 寄存器强行设置为 0x30000000

  5. 运行 (Execution):

    1. 板子复位,CPU 从 0x30000000 开始取指。

    2. 第一条指令通常是跳转到 Reset Handler。

    3. 建立栈,初始化 .bss,搬运 .data

    4. 跳转到 main()

    5. uC/OS-II 启动,LED 开始闪烁。

7.题外话

当然对于静态调试而言反汇编生成一个.dis文件,人类会更容易看懂,可以直接在Makefile文件中添加相关语法。elf的更多作用是对项目架构有一个整体感、配合GDB调试。

arm-none-eabi-objdump -S mini2440_rtos.elf > mixed_view.dis

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐