1. 烧录地址不是“乱填的数字”,而是芯片启动逻辑的物理指纹
你第一次在Keil里点“Download”时,烧录器弹出窗口里那个地址栏——0x08000000、0x6000、0x0000……你是不是下意识就照着例程抄?抄完程序跑起来了,松一口气;哪天换了个芯片,地址一改就卡在复位不起来,连串口都吐不出一个字,才开始怀疑:这地址到底是谁定的?凭什么STM32是0x08000000,而ESP32是0x1000,CH32V203又变成0x0000?网上搜“烧录地址”,一堆人说“看数据手册”,可翻到第37页的Memory Map图,密密麻麻全是寄存器偏移和地址范围,根本找不到“烧录该填哪”这行小字。更让人头大的是,有些工程里Flash起始地址设成0x08000000,但链接脚本里.text段却从0x08004000开始;还有人用ST-Link烧0x08000000成功,用J-Link烧同一地址却报“Verify failed”——地址没变,工具变了,结果就崩了。
这个问题的本质,根本不是“该填什么数字”,而是芯片上电那一刻,硬件如何定位第一条指令的物理位置。它不取决于IDE、不取决于烧录工具、甚至不取决于你的main函数写在哪——它取决于芯片内部的启动机制(Boot Mode)、存储器映射(Memory Mapping)和复位向量表(Reset Vector Table)三者咬合形成的刚性链条。0x08000000不是STM32的“默认值”,它是Cortex-M3内核在系统复位后,从0x00000000这个地址读取主堆栈指针(MSP)值时,被硬件重映射(Remap)后的实际物理Flash起始地址;0x6000不是“小容量芯片的便宜方案”,而是某些8位MCU(比如STC15W4K系列)把内部Flash前24KB按字节寻址,而0x6000恰好是24KB的十六进制表达(24×1024=24576=0x6000);至于0x0000,它既可能是51单片机直接从片内ROM首地址取指,也可能是STM32在Boot0=0/Boot1=0时,将0x00000000重映射到SRAM起始地址,用于调试阶段加载代码。这些地址背后,是芯片厂商在硅片上固化的一套启动规则,是硬件与软件握手的第一句暗号。你填错地址,不是程序烧不进去,而是CPU一上电就往错误的物理空间去取堆栈和指令,就像快递员拿着“北京市朝阳区建国路8号”的单子,却把货送到了“上海市浦东新区世纪大道100号”——地址本身没错,只是它对应的位置,在当前语境下根本不存在收件人。
所以,与其死记硬背几个地址,不如亲手拆开芯片启动流程:从按下复位键那一刻开始,电流如何触发复位电路,Boot引脚状态如何被采样,启动模式选择器如何切换地址总线映射,向量表头两个字如何初始化堆栈和跳转入口……每一个环节都像齿轮咬合,少一个齿,整个系统就停摆。这篇文章,我就带你一层层剥开这层外壳,不讲抽象概念,只讲你手头那块开发板上电瞬间到底发生了什么,以及为什么你昨天烧0x08000000能跑,今天烧0x00000000反而亮了LED——因为后者,恰恰是它真正该去的地方。
2. 地址差异的根源:启动模式、存储器映射与向量表布局三位一体
2.1 启动模式决定“起点坐标系”:Boot引脚是硬件的开关阀
所有地址混乱的起点,都在芯片上电复位时对Boot引脚(通常是BOOT0、BOOT1,或类似命名的GPIO)的采样。这不是软件配置,是纯硬件行为——在VDD稳定、复位信号释放后的第一个时钟周期,芯片内部的启动控制器(Boot ROM或Bootloader)会锁存这些引脚的电平,并据此决定后续指令从哪里取。这个过程发生在任何用户代码运行之前,甚至早于调试器连接。以STM32F103为例,其启动模式由BOOT0和BOOT1共同决定:
| BOOT1 | BOOT0 | 启动模式 | 起始地址映射目标 | 典型用途 |
|---|---|---|---|---|
| x | 0 | 主Flash Memory | 0x08000000 → 0x00000000 | 正常运行用户程序 |
| 0 | 1 | 系统存储器(System Memory) | 0x1FFFF000 → 0x00000000 | 运行厂商预置的ISP Bootloader |
| 1 | 1 | 内置SRAM | 0x20000000 → 0x00000000 | 调试阶段加载代码到RAM执行 |
注意表格中“起始地址映射目标”一栏:所有模式下,CPU复位后始终从0x00000000这个虚拟地址取指。区别在于,不同模式下,0x00000000这个地址被硬件映射到了不同的物理存储器上。当BOOT0=0时,0x00000000被映射到主Flash的起始物理地址0x08000000;当BOOT0=1且BOOT1=0时,0x00000000被映射到系统存储器的物理地址0x1FFFF000。这就是为什么烧录地址必须匹配启动模式——你把程序烧到0x08000000,却把BOOT0拉高,CPU上电后会去0x1FFFF000找代码,自然一片空白。
再看ESP32,它没有传统意义上的Boot引脚,而是通过EFUSE(熔丝)和GPIO6-GPIO11的状态组合来决定启动模式。上电时,芯片内部ROM会检测这些管脚,若为特定组合(如GPIO0=LOW),则进入下载模式,此时UART下载协议要求固件从0x1000地址开始接收;若为正常启动,则从0x00001000(即eFuse中配置的app0分区起始地址)加载应用程序。这里的0x1000不是随意选的,而是为了避开前面存放bootloader和partition table的固定区域(0x0000-0x0FFF)。所以当你看到“ESP32烧录地址是0x1000”,本质上是在告诉下载工具:“请把固件数据,从物理Flash的第4KB位置开始写入”。
提示:很多初学者烧录失败,第一反应是“烧录器坏了”或“芯片坏了”,其实90%的情况是Boot引脚电平没接对。比如STM32最小系统里BOOT0悬空,上电时可能因干扰随机采样为高电平,导致芯片从系统存储器启动,而那里根本没有你的程序。务必用10KΩ电阻将BOOT0可靠拉低(正常启动)或拉高(ISP下载),并用万用表实测确认电平。
2.2 存储器映射是地址的“翻译官”:物理地址与访问地址的双向映射
启动模式选定了“起点”,但CPU访问内存时,并非直接使用物理地址。现代MCU普遍采用存储器映射(Memory Mapping)机制,将物理存储器(Flash、SRAM、外设寄存器)按功能划分,映射到一个统一的、连续的32位地址空间中。这个地址空间就是程序员看到的“地址”。以STM32F103的典型映射为例:
- 0x00000000 - 0x1FFFFFFF:Code区域(可执行代码)
0x00000000 - 0x0000FFFF:Cortex-M3向量表(Vector Table),包含复位向量、中断向量等0x08000000 - 0x0807FFFF:主Flash(64KB),物理存储器
- 0x20000000 - 0x3FFFFFFF:SRAM区域(可读写数据)
0x20000000 - 0x2000FFFF:内置SRAM(64KB)
- 0x40000000 - 0x5FFFFFFF:外设区域(Peripheral)
0x40010000 - 0x40010FFF:GPIOA寄存器组
关键点在于:0x08000000是主Flash的物理地址,但它在Code区域的映射地址也是0x08000000。而0x00000000这个地址,在启动模式为Flash时,被硬件重映射(Remap)指向0x08000000;在启动模式为SRAM时,则被重映射指向0x20000000。这种映射是透明的,CPU指令中的地址都是访问这个统一地址空间的地址,硬件自动完成物理地址转换。
再看8位单片机,比如STC15W4K系列,其存储器映射更简单粗暴:片内Flash直接线性映射到0x0000-0xFFFF地址空间。其最大Flash容量为64KB(0x10000),但实际可用作程序存储的区域通常从0x0000开始,到0x5FFF(24KB)或0x7FFF(32KB)结束,具体取决于型号。因此,当你看到烧录地址是0x6000,很可能是因为该型号的Flash起始地址被定义为0x0000,而0x6000是某个特定功能模块(如IAP擦写区)的起始偏移,或者是Keil C51中为避免覆盖中断向量(0x0000-0x0023)而设置的代码段起始地址。这里没有复杂的重映射,地址就是物理地址,填错就真写到错误位置去了。
注意:链接脚本(Linker Script)中的地址,是告诉编译器“把生成的代码段(.text)放到统一地址空间的哪个位置”。这个位置必须与烧录地址一致,否则烧进去的代码,CPU运行时会从错误地址取指令。例如,若链接脚本设
.text : ORIGIN = 0x08004000, LENGTH = 0x20000,则烧录地址必须是0x08004000,而非0x08000000,否则前16KB的Flash(0x08000000-0x08003FFF)将为空白,复位向量表也就没了。
2.3 向量表是CPU的“导航地图”:地址必须对齐且内容正确
无论地址怎么映射,CPU上电后第一步永远是:从0x00000000(或重映射后的等效地址)读取第一个字(32位),作为主堆栈指针(MSP)初始值;读取第二个字,作为复位中断服务程序(Reset Handler)的入口地址。这两个字,就是向量表(Vector Table)的前两项,位于代码最开头。
向量表有严格要求:
- 必须位于地址0x00000000(或重映射后等效地址)处;
- 必须4字节对齐(每个向量占4字节);
- 复位向量(第二个字)必须指向有效的Reset Handler地址。
假设你用STM32CubeMX生成工程,链接脚本默认将向量表放在0x08000000。那么烧录时,烧录器必须把整个bin文件(包含向量表)从0x08000000开始写入Flash。如果你错误地把烧录地址设为0x08004000,那么0x08000000-0x08003FFF这段Flash就是空白,CPU上电后从0x00000000(映射到0x08000000)读到的MSP值是0xFFFFFFFF,复位向量是0xFFFFFFFF,结果就是堆栈溢出、跳转非法地址,芯片彻底死机。
而有些工程会把向量表放在SRAM中(用于动态修改中断向量),此时链接脚本会将.isr_vector段指定到SRAM起始地址(如0x20000000),同时在启动代码中调用SCB->VTOR = 0x20000000来更新向量表偏移寄存器(VTOR)。这时,烧录地址依然是0x08000000(程序主体),但向量表内容被复制到了SRAM,CPU从VTOR指向的新地址取向量。这种高级用法,更凸显了“烧录地址”与“向量表位置”的分离——烧录地址管的是程序体存放位置,向量表位置管的是CPU取指起点。
3. 实操拆解:三类典型芯片的烧录地址溯源与验证方法
3.1 STM32系列:从参考手册到烧录器配置的完整链路
以STM32F103C8T6(俗称“蓝 pill”)为例,这是最常被问及0x08000000的芯片。我们一步步追溯这个地址的来源:
第一步:查官方参考手册(RM0008)
翻到“Memory mapping”章节(通常在Section 2.3),找到图2:Memory map。图中明确标出:
- Main Flash memory:
0x0800 0000 - 0x0801 FFFF(128 KB) - System memory:
0x1F FFF 000 - 0x1F FFF 7FF(2 KB) - SRAM:
0x2000 0000 - 0x2000 FFFF(64 KB)
第二步:确认启动模式
手册“Boot configuration”章节指出:当BOOT0=0时,启动地址为0x00000000,该地址被重映射到Main Flash的0x08000000。这意味着,CPU从0x00000000取指,实际访问的是物理Flash的0x08000000。
第三步:验证向量表位置
用STM32CubeMX新建工程,选择芯片,生成代码。打开STM32F103xB.ld链接脚本,找到:
MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 0x5000 FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x20000 } SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } > FLASH }这里ORIGIN = 0x08000000明确定义了Flash段起始地址,.isr_vector段(向量表)被链接到此地址开头。
第四步:烧录器配置实操
使用ST-Link Utility烧录:
- 打开.bin文件;
- 在“Target”菜单下,“Settings”中确认“Reset and Run”已勾选;
- 在“Program”页面,“Start Address”输入
0x08000000; - 点击“Program”按钮。
烧录完成后,用ST-Link Utility的“Memory Browser”功能,跳转到0x08000000地址,你会看到前8个字节(两个32位字):
0x08000000:0x20001000(MSP初始值,指向SRAM顶部)0x08000004:0x08000141(Reset Handler地址,末位1表示Thumb指令)
如果烧录地址填错,比如填成0x08004000,那么0x08000000处就是全FF,CPU读到的就是0xFFFFFFFF,必然死机。
实操心得:我曾遇到一个诡异问题——同一份bin文件,用ST-Link烧0x08000000成功,用J-Link烧却Verify失败。排查发现,J-Link的默认Flash算法(Flash Loader)针对的是STM32F103的128KB Flash(0x08000000-0x0801FFFF),而我的芯片是64KB版本(0x08000000-0x0800FFFF)。J-Link在Verify时,会尝试读取0x08010000之后的地址,但那里是未定义区域,返回随机值,导致校验失败。解决方案:在J-Link Commander中执行
exec SetFlashBreakpoint = 0x08000000, 0x00010000,限定校验范围。这说明,烧录地址不仅关乎起点,还关联着整个Flash操作的边界。
3.2 ESP32系列:分区表与OTA机制下的动态地址分配
ESP32的烧录地址逻辑与STM32截然不同,核心在于其分区表(Partition Table)和OTA(Over-The-Air)升级设计。它没有单一的“烧录地址”,而是多个固件镜像分布在Flash的不同区域。
第一步:理解ESP32的Flash布局
ESP32默认使用4MB Flash(0x00000000-0x003FFFFF)。其布局由分区表定义,一个典型的分区表(partitions.csv)如下:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x1E0000, ota_0, app, ota_0, 0x200000, 0x1E0000, ota_1, app, ota_1, 0x3F0000, 0x10000,这里Offset列就是各分区的起始地址。factory分区(出厂固件)从0x10000(64KB)开始,ota_0从0x200000(2MB)开始。
第二步:烧录地址的确定
ESP-IDF的idf.py flash命令,会自动读取partitions.csv,并将固件烧录到factory分区的Offset地址,即0x10000。但这是应用固件的地址。真正的“起点”是bootloader,它被烧录到Flash最开头的0x1000地址(4KB)。bootloader负责读取分区表,找到factory分区,然后跳转执行。
因此,当你用esptool.py手动烧录时:
esptool.py --chip esp32 write_flash 0x1000 bootloader/bootloader.bin(烧bootloader)esptool.py --chip esp32 write_flash 0x10000 firmware.bin(烧应用固件)
这里的0x1000和0x10000,就是由分区表和ESP-IDF构建系统共同约定的。如果你修改了partitions.csv中factory的Offset为0x20000,那么烧录地址就必须改为0x20000。
第三步:验证与调试
烧录后,用esptool.py --chip esp32 read_flash 0x1000 0x1000 bootloader_dump.bin读取bootloader区域,用Hex编辑器打开,可以看到其头部包含magic number(0xE9)和校验和。而应用固件的起始地址(0x10000),其前4字节是0xE9(ESP32固件头标志),紧接着是固件大小、校验和等信息。CPU上电后,首先执行bootloader(0x1000),bootloader解析固件头,确认无误后,才将控制权交给应用固件。
实操心得:在做OTA升级时,我曾把新固件烧录到
ota_0分区(0x200000),但设备重启后依然运行旧固件。原因在于,ESP32的OTA机制依赖于ota_data分区(通常在0x8000)中存储的当前运行分区索引。ota_data分区里记录着“下次启动应运行ota_0还是ota_1”。如果只烧固件不更新ota_data,设备永远不知道该切到新分区。因此,完整的OTA流程是:1. 烧录新固件到目标OTA分区;2. 更新ota_data分区,将索引指向新分区;3. 重启。这再次印证,烧录地址只是数据写入的物理位置,而“运行哪个地址”是由bootloader和ota_data共同决定的。
3.3 8051系列(STC15W4K):线性映射下的地址直觉与陷阱
8051架构简单,没有复杂的存储器映射和重映射,地址即物理地址。但正因如此,新手更容易掉进“地址直觉”的坑里。
以STC15W4K32S4为例,其片内Flash为32KB(0x0000-0x7FFF)。Keil C51的启动代码STARTUP.A51中,默认将代码段(CSEG)起始地址设为0x0000:
?C_STARTUP SEGMENT CODE RSEG ?C_STARTUP ; ... startup code ...链接时,L51工具会将?C_STARTUP段放在0x0000。因此,烧录地址自然是0x0000。
但问题来了:8051的中断向量表固定在0x0000-0x0023(每个中断占8字节,共22个字节)。如果你的main函数或中断服务程序被编译器优化后,代码从0x0000开始紧挨着放,就会覆盖中断向量。所以,很多STC例程会把烧录地址设为0x0024,或者在代码开头强制插入ORG 0x0024,把主程序往后挪。
更隐蔽的陷阱是IAP(In-Application Programming)。STC芯片支持在程序运行时擦写自身Flash。IAP操作需要调用芯片内部的IAP函数,这些函数的入口地址是固定的,比如STC15W4K的IAP触发地址是0x0002。如果你把用户程序烧录到0x0000,那么0x0002这个地址就被你的代码占用了,IAP就失效了。因此,STC官方推荐的IAP安全区,是从0x6000开始(24KB之后),这样前面的0x0000-0x5FFF可以放心放IAP函数和用户代码,互不干扰。
所以,当你看到“STC烧录地址是0x6000”,大概率是在做IAP相关开发,目的是把用户应用程序放在IAP函数之后的安全区域,避免擦写冲突。
实操心得:我调试一个STC15W4K的电磁炉程序时,烧录地址设为0x0000,程序能跑,但无法通过串口升级固件。用STC-ISP软件读取Flash,发现0x0000-0x0023区域被用户代码覆盖,而IAP函数的入口地址0x0002已被改写。解决方案:在Keil中,将
STARTUP.A51的?C_STARTUP段起始地址改为ORG 0x0024,并在链接选项里设置Code区起始为0x0024,烧录地址仍为0x0000。这样,向量表保留,主程序从0x0024开始,IAP函数(通常固化在0x0000-0x0023)完好无损。这说明,对于8051,烧录地址和代码起始地址可以不同,关键是要保证向量表和IAP入口不被覆盖。
4. 常见问题与排查技巧实录:从“烧不进去”到“跑飞了”的全链路诊断
4.1 “烧录成功但不运行”:向量表与启动模式的双重校验
这是最常见也最令人抓狂的问题。烧录器显示“Programming completed successfully”,但LED不亮、串口无输出、调试器连不上。此时,不要急着换芯片,按以下顺序快速排查:
Step 1:确认Boot引脚电平
用万用表直流电压档,测量BOOT0(或对应引脚)对GND电压。正常启动应为0V(GND);ISP下载应为3.3V(VCC)。如果悬空,电压可能在1.5V左右浮动,导致启动模式不确定。立即用10KΩ电阻将其可靠拉低或拉高。
Step 2:检查烧录地址与链接脚本一致性
打开你的链接脚本(.ld或.icf文件),找到ORIGIN参数。例如STM32的FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x20000。确保烧录工具中设置的Start Address与此完全一致。特别注意:有些工具(如OpenOCD)的配置文件里,flash bank命令的地址参数,必须与链接脚本一致。
Step 3:用烧录器读取Flash,验证向量表
以ST-Link Utility为例:
- 连接芯片,点击“Target” -> “Connect”;
- 点击“Memory Browser”,地址栏输入烧录地址(如0x08000000);
- 查看前8个字节(0x08000000和0x08000004):
- 第一个字(MSP)应为一个合理的SRAM地址,如0x20001000(64KB SRAM的顶部);
- 第二个字(Reset Handler)应为一个奇数(Thumb模式),且在Flash范围内,如0x08000141。
如果看到0xFFFFFFFF或0x00000000,说明向量表没烧进去,或烧录地址错了。
Step 4:检查复位电路
用示波器或逻辑分析仪,观察NRST引脚。上电时,应有一个干净的低电平脉冲(>100ns),然后拉高。如果NRST一直为低,或存在毛刺,CPU无法完成复位流程。常见原因是复位电容太大(>100nF)或复位电阻太小(<10KΩ),导致复位时间过长。
排查速查表:
现象 最可能原因 快速验证方法 烧录成功,但完全无反应(LED不亮、串口无声) Boot引脚电平错误或向量表损坏 万用表测BOOT0;Memory Browser看0x08000000处是否为有效向量 烧录成功,串口有乱码或部分输出 MSP初始值错误,导致堆栈溢出 Memory Browser看0x08000000,确认MSP值在SRAM范围内(0x20000000-0x2000FFFF) 烧录成功,能进main但很快死机 Reset Handler地址无效,或中断向量表不完整 Memory Browser看0x08000004,确认其为奇数且指向Flash内有效代码 烧录器报“Verify failed” Flash算法不匹配,或校验范围超出实际Flash大小 在烧录器设置中,将校验长度(Length)设为实际Flash大小(如64KB=0x10000)
4.2 “烧录失败:Target not found”:供电、时钟与接口的底层握手
烧录器根本连不上芯片,提示“Cannot connect to target”、“SWD/JTAG communication failed”。这已经超出了地址范畴,是物理层和协议层的问题。
供电不足是头号杀手。ST-Link或J-Link的SWD接口,需要从目标板取电(VTREF引脚)。如果目标板VDD不稳定(如仅靠USB 5V经LDO降压,而LDO输入电容不足),VTREF电压会跌落,导致通信失败。实测:用万用表测SWDIO和SWCLK引脚对GND电压,正常应在1.8V-3.3V之间。若低于1.5V,优先检查目标板供电。
时钟源缺失。有些芯片(如STM32L系列)在低功耗模式下,会关闭HSE(外部高速晶振)。如果烧录器依赖HSE进行SWD通信,而HSE未起振,连接就会失败。解决方案:在烧录器设置中,强制使用HSI(内部高速RC振荡器)作为调试时钟源;或在目标板上,短接HSE晶振引脚(使其停振),逼迫芯片使用HSI。
接口线路问题。SWD只需两根线(SWDIO、SWCLK)加GND,但布线不当会引入噪声。常见错误:
- SWDIO和SWCLK线上未加100Ω串联电阻(用于阻抗匹配和限流);
- 线长超过15cm,且未做屏蔽;
- SWDIO线上并联了大电容(如滤波电容),导致信号边沿变缓。
独家避坑技巧:我处理过一个批量生产的PCB,10%的板子无法烧录。排查发现,是SWDIO走线经过了一个0.1uF的电源滤波电容的焊盘,该电容在PCB上被错误地跨接到SWDIO和GND之间。虽然电容值很小,但在10MHz的SWD时钟下,容抗已降至159Ω,严重衰减了信号。解决方案:在原理图中,将该电容移到远离SWD走线的位置,并在SWDIO线上增加100Ω电阻。记住:任何靠近SWD/JTAG信号线的电容,都是潜在的“信号杀手”。
4.3 “地址填对了,但功能异常”:链接脚本、分散加载与内存碎片的隐性冲突
程序能跑,但功能错乱:数组越界、全局变量被莫名修改、中断不响应。这往往是链接脚本配置不当,导致内存布局冲突。
案例:RAM不足导致堆栈溢出
STM32F103有20KB SRAM,但链接脚本中LENGTH = 0x5000(20KB)没问题。但如果工程中大量使用malloc,或定义了超大的局部数组(如uint8_t buffer[10000]),而链接脚本未预留足够Heap空间,堆栈(Stack)和堆(Heap)就会相互挤压。解决方法:在链接脚本中,显式定义Heap大小:
_estack = 0x20005000; /* Top of RAM */ _stack_size = 0x400; /* 1KB stack */ _heap_size = 0x1000; /* 4KB heap */并确保_estack - _stack_size - _heap_size大于所有全局变量和静态变量的总和。
案例:分散加载(Scatter Loading)导致外设寄存器访问失败
在ARM Keil MDK中,若使用分散加载文件(.sct),将.data段(已初始化全局变量)放在SRAM,而.bss段(未初始化全局变量)放在另一块SRAM,但启动代码(startup.s)中只初始化了.data,未清零.bss,那么.bss区域的变量就是随机值。这会导致外设初始化失败(如RCC->CR |= RCC_CR_HSEON,但RCC结构体指针未初始化)。务必在startup.s中,加入对.bss段的清零循环。
实操心得:我在移植一个Modbus RTU从站程序到STM32时,发现偶尔接收帧校验失败。最终定位到,是
modbus_buffer数组被链接到了SRAM的末尾,而堆栈向下生长,两者相撞。modbus_buffer被部分覆盖,导致接收的数据错乱。解决方案:在链接脚本中,用ALIGN指令强制将modbus_buffer所在的段,对齐到一个安全的地址边界(如`ALIGN(0x1000