西藏自治区网站建设_网站建设公司_Django_seo优化
2025/12/18 5:35:24 网站建设 项目流程

解释为什么即使加载到0x80000000,程序仍从 TCM 启动。这涉及 bin 文件格式、链接脚本、绝对地址编码和bootaux的工作机制。

一、bin 文件的结构

1. bin 文件是什么?

.bin是纯二进制文件,包含:

  • 机器码(指令)
  • 数据(常量、变量初始值)
  • 地址信息(硬编码的绝对地址)

2. 向量表(Vector Table)的结构

ARM Cortex-M 程序的开头是向量表,结构如下:

地址偏移 内容 说明 ───────────────────────────────────────────── 0x00000000 __initial_sp 初始栈指针(MSP) 0x00000004 Reset_Handler 复位处理函数地址 ← 关键! 0x00000008 NMI_Handler NMI中断处理函数地址 0x0000000C HardFault_Handler 硬错误处理函数地址 ... ... 其他中断处理函数

向量表的前两个条目:

  1. __initial_sp:初始栈指针值
  2. Reset_Handler:程序入口地址(PC 的初始值)

3. 关键点:地址是硬编码的

编译时,链接器会把函数地址写入向量表。例如:

TCM版本编译时:

// 链接脚本:MIMX8ML8xxxxx_cm7_ram.ldMEMORY{m_interrupts:ORIGIN=0x00000000,...// TCM地址m_text:ORIGIN=0x00000400,...}// Reset_Handler函数被链接到地址 0x000004CD// 向量表中写入:0x000004CD

DDR版本编译时:

// 链接脚本:MIMX8ML8xxxxx_cm7_ddr_ram.ldMEMORY{m_interrupts:ORIGIN=0x80000000,...// DDR地址m_text:ORIGIN=0x80000400,...}// Reset_Handler函数被链接到地址 0x800004CD// 向量表中写入:0x800004CD

二、链接脚本的作用

1. 链接脚本决定地址

链接脚本告诉链接器:

  • 代码放在哪个地址
  • 数据放在哪个地址
  • 函数地址如何计算

2. 地址编码到机器码中

编译过程:

源代码 (hello_world.c) ↓ [编译] 目标文件 (.o) - 包含未解析的符号 ↓ [链接] ← 链接脚本在这里起作用! 可执行文件 (.elf) - 所有地址已确定 ↓ [objcopy] 二进制文件 (.bin) - 纯二进制,地址已硬编码

链接时,链接器会:

  1. 读取链接脚本,确定每个段的地址
  2. 计算所有函数和变量的绝对地址
  3. 将这些地址写入机器码

3. 示例:函数调用

// 源代码voidmain(void){PRINTF("hello");}// 编译后的机器码(简化)// 假设PRINTF函数在地址0x00001234(TCM版本)// 或者地址0x80001234(DDR版本)// 调用PRINTF的指令:BL0x00001234← TCM版本:硬编码的TCM地址// 或BL0x80001234← DDR版本:硬编码的DDR地址

三、为什么加载到0x80000000仍从 TCM 启动?

1. 实际发生的过程

ext4load mmc2:2 0x80000000 /home/root/ecspi_loopback.bin

这一步做了什么:

  • 从 eMMC 读取 bin 文件
  • 将文件内容按字节顺序写入 DDR 的0x80000000

bin 文件内容(TCM版本):

地址偏移 内容(十六进制) 含义 ───────────────────────────────────────────── 0x0000 0x20020000 __initial_sp (栈指针) 0x0004 0x000004CD Reset_Handler地址 ← 这是TCM地址! 0x0008 0x00000501 NMI_Handler地址 ... 0x04CD 0xB510 Reset_Handler的机器码 0x04CE 0x4801 ... ...

加载到0x80000000后,内存内容:

DDR地址 内容 含义 ───────────────────────────────────────────── 0x80000000 0x20020000 __initial_sp 0x80000004 0x000004CD Reset_Handler地址 ← 还是TCM地址! 0x80000008 0x00000501 NMI_Handler地址 ...

2.bootaux命令的工作机制

bootaux 0x80000000

bootaux做了什么:

// bootaux的伪代码(简化)voidbootaux(uint32_taddress){// 1. 从指定地址读取向量表uint32_t*vector_table=(uint32_t*)address;// 2. 读取初始栈指针uint32_tinitial_sp=vector_table[0];// 0x80000000处的内容// 3. 读取Reset_Handler地址(这是关键!)uint32_treset_handler=vector_table[1];// 0x80000004处的内容// 对于TCM版本的bin:reset_handler = 0x000004CD(TCM地址)// 对于DDR版本的bin:reset_handler = 0x800004CD(DDR地址)// 4. 设置M7的栈指针M7_MSP=initial_sp;// 5. 设置M7的程序计数器(PC)M7_PC=reset_handler;// ← 跳转到这个地址执行// 6. 启动M7核start_M7_core();}

3. 问题所在

对于 TCM 版本的 bin 文件:

步骤1: ext4load mmc 2:2 0x80000000 ecspi_loopback.bin ↓ 将bin文件内容复制到DDR的0x80000000 步骤2: bootaux 0x80000000 ↓ 从0x80000004读取Reset_Handler地址 ↓ 读取到:0x000004CD(这是TCM地址!) ↓ 设置PC = 0x000004CD ↓ M7从TCM的0x000004CD开始执行

即使 bin 文件在 DDR 的0x80000000,向量表中硬编码的地址仍是 TCM 地址,所以 PC 会跳回 TCM。

四、地址映射关系

1. M7 的地址空间

M7本地地址空间(M7视角) 系统地址空间(A53/U-Boot视角) ───────────────────────── ─────────────────────────── 0x00000000 (ITCM) → 0x007E0000 0x20000000 (DTCM) → 0x1F800000 0x40000000 (DDR区域1) → 0x40000000 0x80000000 (DDR区域2) → 0x80000000

2. 为什么0x000004CD能执行?

M7 执行时使用本地地址空间:

  • 0x000004CD在 M7 本地地址空间中是 ITCM
  • 即使从系统地址0x7E0000加载,M7 执行时仍按本地地址0x000004CD访问 ITCM

3. 地址不匹配的问题

U-Boot加载位置:0x80000000(系统地址,DDR区域2) ↓ bin文件内容:向量表中Reset_Handler = 0x000004CD(TCM地址) ↓ bootaux读取:PC = 0x000004CD ↓ M7执行:从0x000004CD(TCM)开始执行 ↓ 但是:代码实际在DDR的0x80000000,不在TCM! ↓ 结果:程序可能崩溃或执行错误代码

五、为什么 TCM 版本能工作?

1. 正确的加载流程

ext4load mmc2:2 0x48000000 hello_world.bin# 临时加载到DDRcp.b 0x48000000 0x7e0000${filesize}# 复制到TCM系统地址bootaux 0x7e0000# 从TCM启动

过程:

步骤1: ext4load → 0x48000000(DDR临时缓冲区) 步骤2: cp.b → 0x7e0000(TCM的系统地址) ↓ 地址映射:0x7E0000(系统)→ 0x00000000(M7本地) ↓ 代码被复制到M7的ITCM(0x00000000开始) 步骤3: bootaux 0x7e0000 ↓ 从0x7e0004读取Reset_Handler = 0x000004CD ↓ PC = 0x000004CD ↓ M7从ITCM的0x000004CD执行 ← 代码就在这里! ↓ 成功!

2. 为什么需要cp.b

因为:

  • ext4load只能加载到 DDR(0x48000000
  • TCM 版本的程序必须运行在 TCM
  • 需要将代码从 DDR 复制到 TCM

六、DDR 版本如何工作?

1. DDR 版本的链接脚本

// MIMX8ML8xxxxx_cm7_ddr_ram.ld MEMORY { m_interrupts : ORIGIN = 0x80000000, LENGTH = 0x00000400 m_text : ORIGIN = 0x80000400, LENGTH = 0x001FFC00 }

编译后:

  • 向量表地址:0x80000000
  • Reset_Handler 地址:0x800004CD(DDR 地址)

2. DDR 版本的 bin 文件内容

地址偏移 内容(十六进制) 含义 ───────────────────────────────────────────── 0x0000 0x80200000 __initial_sp (DDR中的栈) 0x0004 0x800004CD Reset_Handler地址 ← DDR地址! 0x0008 0x80000501 NMI_Handler地址 ...

3. DDR 版本的启动流程

ext4load mmc2:2 0x80000000 ecspi_loopback_ddr.bin bootaux 0x80000000

过程:

步骤1: ext4load → 0x80000000(DDR区域2) ↓ 代码直接加载到DDR的0x80000000 步骤2: bootaux 0x80000000 ↓ 从0x80000004读取Reset_Handler = 0x800004CD ↓ PC = 0x800004CD ↓ M7从DDR的0x800004CD执行 ← 代码就在这里! ↓ 成功!

七、核心原理总结

1. bin 文件包含硬编码地址

  • 链接时,链接器根据链接脚本确定所有地址
  • 这些地址被硬编码到机器码中
  • bin 文件是纯二进制,不包含重定位信息

2. 地址必须匹配

加载地址 ≠ 运行地址 → 程序无法运行 加载地址 = 运行地址 → 程序可以运行

3. 为什么不能混用?

TCM版本的bin: - 链接地址:0x00000000(TCM) - 向量表中Reset_Handler:0x000004CD(TCM地址) - 必须加载到:0x7e0000(系统地址,映射到TCM) - 不能加载到:0x80000000(DDR地址不匹配) DDR版本的bin: - 链接地址:0x80000000(DDR) - 向量表中Reset_Handler:0x800004CD(DDR地址) - 必须加载到:0x80000000(DDR地址) - 不能加载到:0x7e0000(TCM地址不匹配)

4. 类比理解

想象bin文件是一张地图: TCM版本的bin文件: "Reset_Handler在TCM的0x000004CD号房间" 即使你把地图放在DDR的0x80000000号房间, 地图上写的还是"去TCM的0x000004CD号房间" 所以程序还是会去TCM执行! DDR版本的bin文件: "Reset_Handler在DDR的0x800004CD号房间" 地图放在DDR的0x80000000号房间, 地图上写"去DDR的0x800004CD号房间" 所以程序在DDR执行!

八、如何验证?

方法1:查看 bin 文件内容

# 使用hexdump或xxd查看bin文件的前几个字节hexdump -C ecspi_loopback.bin|head-5# TCM版本会看到:# 00000000 00 00 20 20 cd 04 00 00 ... ← Reset_Handler = 0x000004CD# DDR版本会看到:# 00000000 00 00 20 80 cd 04 00 80 ... ← Reset_Handler = 0x800004CD

方法2:查看链接脚本

# TCM版本使用:MIMX8ML8xxxxx_cm7_ram.ld → m_interrupts:ORIGIN=0x00000000# DDR版本使用:MIMX8ML8xxxxx_cm7_ddr_ram.ld → m_interrupts:ORIGIN=0x80000000

方法3:查看编译输出

# 编译时查看map文件# TCM版本:Reset_Handler地址在0x0000xxxx# DDR版本:Reset_Handler地址在0x8000xxxx

九、总结

  • 原因:bin 文件中的地址是链接时硬编码的,由链接脚本决定。
  • 为什么加载到0x80000000仍从 TCM 启动:向量表中的Reset_Handler地址是 TCM 地址(0x000004CD),bootaux读取后设置 PC 为该地址,因此跳回 TCM。
  • 原理:
    1. 链接脚本决定代码的链接地址
    2. 链接器将地址硬编码到机器码
    3. bin 文件包含这些硬编码地址
    4. bootaux从向量表读取入口地址并设置 PC
    5. 如果地址不匹配,程序无法正确执行

因此,必须使用与链接地址匹配的加载地址,TCM 版本加载到 TCM,DDR 版本加载到 DDR。


解释编译和链接的作用、原理和过程。

[4 tools called]

解释编译和链接的作用、原理和过程。

一、整体流程概览

源代码文件 (.c, .h, .S) ↓ [编译 Compilation] ↓ 目标文件 (.o) - 包含机器码,但地址未确定 ↓ [链接 Linking] ↓ 可执行文件 (.elf) - 所有地址已确定 ↓ [objcopy] ↓ 二进制文件 (.bin) - 纯二进制,可直接运行

二、编译(Compilation)的作用

1. 编译做什么?

编译将源代码(.c.S)转换为目标文件(.o),生成机器码,但地址尚未确定。

2. 编译的输入和输出

输入:

  • 源代码文件(.c.S
  • 头文件(.h
  • 编译器选项(-O2-g-mcpu=cortex-m7等)

输出:

  • 目标文件(.o
  • 包含机器码、符号表、重定位信息

3. 编译过程详解

步骤1:预处理(Preprocessing)
// hello_world.c#include"fsl_debug_console.h"// 预处理会展开这个头文件#include"ad7606.h"intmain(void){AD7606_Init();// 调用AD7606_Init函数return0;}

预处理后(简化):

// 展开头文件内容// fsl_debug_console.h的内容被插入// ad7606.h的内容被插入intmain(void){AD7606_Init();// 函数声明来自ad7606.hreturn0;}
步骤2:编译(Compilation)

编译器将 C 代码转换为汇编代码:

// C代码intmain(void){AD7606_Init();return0;}

转换为汇编(简化):

main: push {lr} ; 保存返回地址 bl AD7606_Init ; 调用AD7606_Init(地址未知!) mov r0, #0 ; return 0 pop {pc} ; 返回
步骤3:汇编(Assembly)

汇编器将汇编代码转换为机器码:

bl AD7606_Init ; 汇编指令

转换为机器码(简化):

机器码:0xF000F000 (BL指令的编码) 目标地址:AD7606_Init ← 这是符号,地址还不知道!

4. 目标文件(.o)的结构

目标文件包含:

1) 机器码段(.text)
// hello_world.c编译后的.obj文件.text段: 地址偏移 机器码 对应的源代码 ──────────────────────────────────────0x00000xB500push{lr}0x00020xF000F000bl AD7606_Init ← 地址是占位符!0x00060x2000mov r0,#00x00080xBD00pop{pc}
2) 符号表(Symbol Table)
符号表: 符号名 类型 地址(相对) 说明 ────────────────────────────────────────────── main 函数0x0000定义在本文件 AD7606_Init 函数???未定义(需要从其他文件找) PRINTF 函数???未定义(需要从库中找)
3) 重定位表(Relocation Table)
重定位表: 位置 类型 符号名 说明 ──────────────────────────────────────────────0x0004R_ARM_CALL AD7606_Init 需要填入AD7606_Init的地址0x0010R_ARM_CALL PRINTF 需要填入PRINTF的地址

5. 为什么地址未确定?

编译时,编译器不知道:

  • 其他文件中的函数在哪里
  • 库函数在哪里
  • 全局变量在哪里
  • 最终程序会放在哪个内存地址

因此,编译只能:

  • 生成相对地址的机器码
  • 记录需要重定位的位置
  • 记录未解析的符号

三、链接(Linking)的作用

1. 链接做什么?

链接将多个目标文件(.o)合并成一个可执行文件(.elf),完成:

  • 解析所有符号(函数、变量)
  • 确定所有地址
  • 重定位代码和数据
  • 合并所有段

2. 链接的输入和输出

输入:

  • 多个目标文件(.o
  • 库文件(.a
  • 链接脚本(.ld

输出:

  • 可执行文件(.elf
  • 包含所有代码和数据,地址已确定

3. 链接过程详解

步骤1:收集所有目标文件
链接器收集: hello_world.c.obj ad7606.c.obj pin_mux.c.obj board.c.obj fsl_ecspi.c.obj (来自SDK库) fsl_gpio.c.obj (来自SDK库) startup_MIMX8ML8_cm7.S.obj ...
步骤2:读取链接脚本
// MIMX8ML8xxxxx_cm7_ram.ld MEMORY { m_interrupts : ORIGIN = 0x00000000, LENGTH = 0x00000400 m_text : ORIGIN = 0x00000400, LENGTH = 0x0001FC00 m_data : ORIGIN = 0x20000000, LENGTH = 0x00020000 } SECTIONS { .interrupts : { *(.isr_vector) } > m_interrupts .text : { *(.text) } > m_text .data : { *(.data) } > m_data }

链接脚本告诉链接器:

  • 代码放在0x00000400(TCM)
  • 数据放在0x20000000(DTCM)
  • 向量表放在0x00000000
步骤3:符号解析(Symbol Resolution)

链接器查找所有符号的定义:

符号查找过程: 1. 查找 AD7606_Init - 在 hello_world.c.obj 中:未定义(只是调用) - 在 ad7606.c.obj 中:找到了!地址待定 2. 查找 PRINTF - 在所有.obj文件中:未定义 - 在库文件 libc.a 中:找到了! 3. 查找 Reset_Handler - 在 startup_MIMX8ML8_cm7.S.obj 中:找到了!
步骤4:地址分配(Address Assignment)

链接器根据链接脚本分配地址:

地址分配过程: 1. 向量表(.interrupts段) 起始地址:0x00000000 - __initial_sp: 0x00000000 - Reset_Handler: 0x00000004 - NMI_Handler: 0x00000008 ... 2. 代码段(.text段) 起始地址:0x00000400 - Reset_Handler函数:0x000004CD - SystemInit函数:0x00000501 - AD7606_Init函数:0x00001234 - main函数:0x00001500 - PRINTF函数(库):0x00002000 3. 数据段(.data段) 起始地址:0x20000000 - 全局变量:0x20000000 ...
步骤5:重定位(Relocation)

链接器修改机器码中的地址:

重定位过程: 1. hello_world.c.obj中的调用: 原始机器码:bl AD7606_Init (地址未知) ↓ 链接器计算:AD7606_Init = 0x00001234 ↓ 修改机器码:bl 0x00001234 (地址已确定) 2. hello_world.c.obj中的调用: 原始机器码:bl PRINTF (地址未知) ↓ 链接器计算:PRINTF = 0x00002000 ↓ 修改机器码:bl 0x00002000 (地址已确定)
步骤6:合并段(Section Merging)

链接器将所有目标文件的段合并:

合并过程: 所有.obj文件的.text段 → 合并到最终.elf的.text段 所有.obj文件的.data段 → 合并到最终.elf的.data段 所有.obj文件的.bss段 → 合并到最终.elf的.bss段

四、实际例子

例子1:函数调用

源代码
// hello_world.c#include"ad7606.h"intmain(void){AD7606_Init();// 调用其他文件中的函数return0;}
// ad7606.cvoidAD7606_Init(void){// 初始化代码}
编译后
// hello_world.c.obj符号表: main:定义,地址0x0000(相对地址) 重定位表: 位置0x0004:需要填入AD7606_Init的地址 机器码:0x0000:push{lr}0x0002:bl???← AD7606_Init的地址未知0x0006:mov r0,#00x0008:pop{pc}
// ad7606.c.obj符号表: AD7606_Init:定义,地址0x0000(相对地址) 机器码:0x0000:push{r4,lr}0x0002:...初始化代码...0x0050:pop{r4,pc}
链接后
// hello_world.elf链接器的工作:1.符号解析:-AD7606_Init在ad7606.c.obj中找到2.地址分配(根据链接脚本):-main函数:0x00001500-AD7606_Init函数:0x000012343.重定位:-hello_world.c.obj中0x0002处的bl指令-修改为:bl0x00001234最终机器码:0x1500:push{lr}0x1502:bl0x00001234← 地址已确定!0x1506:mov r0,#00x1508:pop{pc}0x1234:push{r4,lr}← AD7606_Init的代码0x1236:...初始化代码...0x1284:pop{r4,pc}

例子2:全局变量

源代码
// ad7606.cAD7606_FIFO_T g_tAD7606;// 全局变量voidAD7606_Init(void){g_tAD7606.usRead=0;// 使用全局变量}
// hello_world.cexternAD7606_FIFO_T g_tAD7606;// 声明外部变量intmain(void){g_tAD7606.usWrite=0;// 使用全局变量return0;}
编译后
// ad7606.c.obj符号表: g_tAD7606:定义,大小16字节.bss段(未初始化数据): 预留16字节空间(地址未定)// hello_world.c.obj符号表: g_tAD7606:未定义(需要从其他文件找) 重定位表: 位置0x0010:需要填入g_tAD7606的地址 机器码: ldr r0,=g_tAD7606 ← 地址未知 mov r1,#0str r1,[r0]
链接后
// hello_world.elf链接器的工作:1.符号解析:-g_tAD7606在ad7606.c.obj中找到2.地址分配(根据链接脚本):-.bss段起始:0x20000000-g_tAD7606:0x200000003.重定位:-hello_world.c.obj中的ldr指令-修改为:ldr r0,=0x20000000最终机器码: ldr r0,=0x20000000← 地址已确定! mov r1,#0str r1,[r0].bss段(0x20000000): 预留16字节给g_tAD7606

五、为什么需要两个步骤?

1. 分离编译的好处

好处1:提高编译效率
如果只有一个文件修改: - 只需要重新编译这个文件 - 其他文件不需要重新编译 - 只需要重新链接 如果所有文件一起编译: - 任何文件修改都要重新编译所有文件 - 效率很低
好处2:模块化开发
不同的人可以: - 同时开发不同的.c文件 - 各自编译成.obj文件 - 最后一起链接 就像搭积木: - 每个人做自己的积木块(.obj文件) - 最后组装在一起(链接)
好处3:代码复用
SDK库文件: - fsl_ecspi.c → fsl_ecspi.obj → libdriver.a - fsl_gpio.c → fsl_gpio.obj → libdriver.a 你的程序: - hello_world.c → hello_world.obj - ad7606.c → ad7606.obj 链接时: - 只需要链接用到的库函数 - 不需要的库函数不会被包含

2. 链接的必要性

必要性1:地址必须确定
编译时: - 不知道其他文件的函数在哪里 - 不知道库函数在哪里 - 不知道最终运行地址 链接时: - 读取链接脚本,知道内存布局 - 可以计算所有地址 - 可以修改机器码中的地址
必要性2:符号必须解析
编译时: - 只知道函数名(符号) - 不知道函数在哪里 链接时: - 在所有.obj文件中查找符号 - 在库文件中查找符号 - 如果找不到,报错:undefined reference

六、链接脚本的作用

1. 链接脚本决定什么?

// MIMX8ML8xxxxx_cm7_ram.ld MEMORY { m_interrupts : ORIGIN = 0x00000000, LENGTH = 0x00000400 m_text : ORIGIN = 0x00000400, LENGTH = 0x0001FC00 m_data : ORIGIN = 0x20000000, LENGTH = 0x00020000 }

链接脚本告诉链接器:

  • 代码放在哪里(0x00000400
  • 数据放在哪里(0x20000000
  • 每个段的大小限制

2. 为什么需要链接脚本?

因为:

  • 不同MCU有不同的内存布局
  • 不同应用需要不同的内存分配
  • 链接器需要知道如何组织代码和数据

3. 链接脚本如何影响地址?

TCM版本的链接脚本: m_text: ORIGIN = 0x00000400 ← TCM地址 链接结果: - Reset_Handler: 0x000004CD - AD7606_Init: 0x00001234 - main: 0x00001500 DDR版本的链接脚本: m_text: ORIGIN = 0x80000400 ← DDR地址 链接结果: - Reset_Handler: 0x800004CD - AD7606_Init: 0x80001234 - main: 0x80001500

七、完整的编译链接流程

实际例子:编译 hello_world

步骤1:预处理
arm-none-eabi-gcc -E hello_world.c -o hello_world.i

结果:展开所有#include和宏定义

步骤2:编译
arm-none-eabi-gcc -c hello_world.c -o hello_world.o

结果:生成hello_world.o,包含:

  • 机器码(地址未定)
  • 符号表
  • 重定位表
步骤3:链接
arm-none-eabi-gcc hello_world.o ad7606.o... -T MIMX8ML8xxxxx_cm7_ram.ld -o hello_world.elf

结果:生成hello_world.elf,包含:

  • 所有代码(地址已确定)
  • 所有数据(地址已确定)
  • 符号表(用于调试)
步骤4:生成bin文件
arm-none-eabi-objcopy -O binary hello_world.elf hello_world.bin

结果:生成hello_world.bin,纯二进制文件

八、常见问题

1. 为什么会有 “undefined reference” 错误?

原因: - 链接时找不到某个符号的定义 - 比如:调用了AD7606_Init,但链接时找不到这个函数 解决方法: - 确保包含定义了该函数的.obj文件 - 或者链接包含该函数的库文件

2. 为什么会有 “multiple definition” 错误?

原因: - 同一个符号在多个文件中定义了 - 比如:g_tAD7606在ad7606.c和hello_world.c中都定义了 解决方法: - 只在一个文件中定义 - 其他文件用extern声明

3. 为什么链接脚本很重要?

原因: - 链接脚本决定了程序的地址布局 - 地址必须匹配实际的硬件内存布局 - 地址不匹配会导致程序无法运行

九、总结

编译(.c → .o)

作用:

  • 将源代码转换为机器码
  • 生成符号表和重定位信息

需要:

  • 源代码文件
  • 头文件
  • 编译器选项

做了什么:

  • 预处理(展开头文件)
  • 编译(C → 汇编)
  • 汇编(汇编 → 机器码)

输出:

  • 目标文件(.o
  • 包含机器码,但地址未确定

链接(.o → .elf)

作用:

  • 合并多个目标文件
  • 解析所有符号
  • 确定所有地址

需要:

  • 多个目标文件(.o
  • 库文件(.a
  • 链接脚本(.ld

做了什么:

  • 符号解析(查找所有符号的定义)
  • 地址分配(根据链接脚本分配地址)
  • 重定位(修改机器码中的地址)
  • 段合并(合并所有段)

输出:

  • 可执行文件(.elf
  • 包含所有代码和数据,地址已确定

为什么需要两个步骤?

  1. 提高效率:只重新编译修改的文件
  2. 模块化:不同文件可以独立开发
  3. 代码复用:可以链接库文件
  4. 地址确定:链接时才能确定最终地址

这就是为什么即使把 TCM 版本的 bin 文件加载到0x80000000,程序还是会从 TCM 启动——因为链接时已经将地址硬编码为 TCM 地址了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询