本章记录什么 这一章把前两章的寄存器规则和调用规范放进真正的启动路径:镜像被放到什么地址,向量表为何必须对齐,链接脚本如何给代码、数据、页表和各模式栈分配空间,启动汇编又如何把一个没有 C 运行时的处理器带到 kernel_main。这是后续一切功能的地基。
在这一章之前做了什么 此时已经知道处理器模式会选择不同的 banked sp,也知道调用 C 前必须满足 AAPCS 的栈对齐和寄存器约定。但这些知识仍是静态规则。启动阶段要把它们变成具体地址:先屏蔽异步事件,建立向量基址和每模式栈,清理 BSS,再由 CPU0 进入共享初始化;CPU1 不能重复执行同一套破坏性操作。
本章会从链接合同讲到第一行串口输出,并给出一份可独立阅读的启动汇编。代码不是“最终模板”,而是一条可以逐条断点验证的最小路径。
系列目录 · 上一篇:AAPCS32 与关键汇编
本章的阅读顺序 先读基础概念,确认每个术语解决什么问题;再看设计推导,理解实现为什么采用这种保存集合、初始化顺序或状态机;随后逐段阅读代码和执行轨迹;最后用验证方法与错误案例检查理解。文中保留寄存器名、指令名和标准缩写,叙述部分统一使用简体中文。
从构建骨架到第一行 C 输出 实现步骤一:建立 ARM 交叉构建骨架 1 d256fdc build: add the ARM cross-build skeleton
这一小步只解决什么 只证明:ARM 工具链能够把一条最小 Cortex-A9 入口编译、链接成 ELF 和 BIN。
此时不做 UART、不进 C、不开 IRQ。这样构建失败时,问题只可能集中在工具链、编译参数、 链接脚本或入口符号,而不是几十个硬件模块。
先看什么 原系统:
1 2 3 src/CMakeLists.txt src/toolchain.cmake src/boot/linker.lds
RTEMS 参考:
1 2 rtems-6.2/spec/build/bsps/arm/xilinx-zynq/linkcmds.yml rtems-6.2/bsps/arm/shared/start/linkcmds.base
官方资料重点:
Cortex-A9 使用 ARMv7-A 指令集;
AAPCS32 的 8 字节公开接口栈对齐;
Zynq DDR 地址布局;
ELF 入口、VMA/LMA、.text/.data/.bss 的区别。
必须先回答的问题
为什么不能继续使用 x86_64-linux-gcc?
为什么先选 -mcpu=cortex-a9 -marm -mfloat-abi=soft?
为什么裸机需要 -ffreestanding -nostdlib -nostartfiles?
为什么链接地址暂定 0x00100000,而不是盲目使用地址 0?
为什么生成 ELF 还要生成 BIN 和 map 文件?
这一步 修改什么 1 2 3 4 5 6 ports/zynq7020/CMakeLists.txt ports/zynq7020/cmake/arm-none-eabi-toolchain.cmake ports/zynq7020/linker.ld ports/zynq7020/src/start.S ports/zynq7020/scripts/build.sh ports/zynq7020/scripts/run_qemu.sh
最小 _start 只做两件事:屏蔽 IRQ/FIQ,然后 wfi 停住。此时八个向量都临时回到 同一停机入口,这不是最终实现,只是为了先让工具链闭环。
怎样验证 1 2 3 cd ports/zynq7020 ./scripts/build.sh arm-none-eabi-readelf -h -l build/zynq7020.elf
应确认:
ELF 是 elf32-littlearm;
Entry 指向 _start;
代码 LOAD 段是 R E,数据/栈段是 RW,没有 RWX;
构建没有 executable-stack 警告。
常见错误
CMake 在 project() 之后才设置工具链,导致先使用宿主编译器;
仍保留 -mcmodel=large/-mno-red-zone 等 x86 参数;
链接脚本没有 ENTRY(_start);
把 build 目录提交进 Git;
看到能生成 ELF 就误认为已经能启动。
实现步骤二:从汇编启动进入可观察的 C 环境 1 d099bd4 boot: enter C with mode stacks and UART output
这一小步解决什么 建立最小启动合同:
1 2 3 4 5 6 ELF入口 → 设置VBAR → 初始化各模式banked SP → 清零.bss → 进入kernel_main → UART打印证据
先看什么 对应的基础资料:
1 2 3 处理器模式与寄存器组 AAPCS32 调用规范 异常入口与返回规则
RTEMS 参考:
1 2 3 4 rtems-6.2/bsps/arm/shared/start/start.S rtems-6.2/bsps/arm/xilinx-zynq/start/bspstarthooks.c rtems-6.2/bsps/include/dev/serial/zynq-uart-zynq.h rtems-6.2/bsps/include/dev/serial/zynq-uart-regs.h
官方资料重点:
ARM ARM B1.3:模式与 banked registers;
ARM ARM B1.8:异常向量和异常入口;
UG585 UART Controller、地址映射与 Zynq PS UART 寄存器。
必须先回答的问题
为什么 SVC、IRQ、FIQ、ABT、UND 必须有各自有效 SP?
cps #0x12 为什么只是切换当前看到的 banked SP,而不是触发 IRQ?
为什么调用 C 以前必须确保 SVC SP 八字节对齐?
.bss 为什么在 ELF 中不保存一大块零,却必须由启动代码清零?
如果没有 UART,启动停在哪里应该怎样判断?
推荐实现顺序
在链接脚本中定义向量、代码、数据、BSS 和五个模式栈。
_start 中先 cpsid if,避免半初始化状态收到中断。
写 VBAR,并执行 DSB/ISB。
逐模式执行 cps,为对应 banked SP 赋值。
回到 SVC,按字清零 __bss_start..__bss_end。
bl kernel_main,若意外返回则进入 WFI 循环。
初始化 UART0,再打印 MPIDR、CPSR、段地址和 BSS 自检。
怎样验证 运行 QEMU 后至少应看到:
1 2 3 4 5 CPU0 reached C entry MPIDR=0x80000000 image_start=0x00100000 PASS: .bss cleared PASS: vector table, mode stacks, UART0 and C entry are alive
如果完全无输出,按以下顺序查:
1 2 3 4 5 ELF入口和链接地址 → _start是否执行 → SVC SP是否有效和对齐 → .bss循环是否越界 → UART基址/状态位/时钟参数
不要一上来怀疑 C 语法或 GIC,因为此时还没有启用 GIC。
启动时序和源码职责 2. 源码职责
路径
作用
CMakeLists.txt
Cortex-A9 编译链接、ELF/BIN/Map、QEMU/GDB 目标
linker.ld
RX/RW 段、每核各模式栈、L1/L2 转换表
src/start.S
向量、MPIDR 分流、per-CPU 栈、CPU1 等待、IRQ 保存/恢复
src/mmu.c
页表构造、TTBR0/TTBCR/DACR/SCTLR、TLB 与 PAR 查询
src/cache.c
L1 set/way、I-Cache、SCU、ACTLR.SMP、L2C-310
src/context.c
普通任务初始上下文
src/context_switch.S
AAPCS 同步 context save/restore
src/console.c
ANSI 启动页、实时状态仪表盘和纯文本回退
src/global_timer.c
轮询 Cortex-A9 Global Timer,为运行仪表盘提供无 IRQ 实时时基
src/scheduler.c
任务私有 IRQ frame、need_reschedule、IRQ 尾部轮转
src/irq.c
GIC 注册/分发、PPI29、SGI、EOI、每核 IRQ 软件栈监测
src/smp.c
CPU1 release、次核初始化、online 状态和 SGI handshake
src/kernel.c
串联全部可观察验收测试
src/uart.c
Zynq PS UART0 轮询输出
scripts/build.sh
生成 Debug ELF/BIN/Map
scripts/run_qemu.sh
双核普通运行
scripts/run_qemu_debug.sh
双核暂停启动并监听 GDB
3. 启动与双核时序 1 2 3 4 5 6 7 8 9 QEMU/Boot loader 把 ELF 放到 0x00100000 → 两核进入 _start,关 IRQ/FIQ,设置 VBAR → 根据 MPIDR 为每个模式选择 CPU0/CPU1 专属栈 → CPU1 等待 secondary_release → CPU0 清 .bss,进入 kernel_main → CPU0 建 MMU,开 SCU/L1/L2,初始化 GIC Distributor/GICC → CPU0 发布 release flag,clean cache,写 Zynq kick address,SEV → CPU1 建自己的 TTBR/MMU/L1/ACTLR.SMP/GICC → 两核用 SGI0 往返确认,CPU1 随后 WFI
真实 Zynq-7000 的次核 kick address 为 0xfffffff0;QEMU 两核本来就从同一入口启动, 因此 CPU1 先在软件等待点停住。两种路径共享后续初始化,但真板仍需要由 FSBL/U-Boot 或 JTAG 保证 DDR、镜像地址、CPU1 状态和时钟满足交接合同。
可独立阅读的启动与异常向量实现 下面给出文章对应的完整实现片段。即使没有配套仓库,也可以沿着注释、寄存器访问和调用关系逐行阅读。代码之后的分析只讨论本章主题,不要求预先掌握其他目录结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 .syntax unified .cpu cortex-a9 .arm #include "smp_constants.h" .section .vectors, "ax", %progbits .align 5 .global _vectors .type _vectors, %function _vectors: b _start b undefined_handler b svc_handler b prefetch_abort_handler b data_abort_handler b reserved_handler b irq_entry b fiq_handler .size _vectors, . - _vectors .section .text.startup, "ax", %progbits .align 4 .global _start .type _start, %function _start: @ 启动期间先关 IRQ/FIQ,避免各模式栈尚未建立时进入异常。 cpsid if @ VBAR 指向本镜像的异常向量表。 ldr r0, =_vectors mcr p15, 0, r0, c12, c0, 0 dsb isb @ QEMU的两个核从同一入口启动;MPIDR.Aff0作为每核栈和路径索引。 mrc p15, 0, r4, c0, c0, 5 and r4, r4, #0x3 @ ARMv7-A 的异常模式拥有 banked SP,必须逐个切换模式后赋值。 cps #0x1b cmp r4, #0 ldreq sp, =__stack_und_cpu0_top ldrne sp, =__stack_und_cpu1_top cps #0x17 cmp r4, #0 ldreq sp, =__stack_abt_cpu0_top ldrne sp, =__stack_abt_cpu1_top cps #0x12 cmp r4, #0 ldreq sp, =__stack_irq_cpu0_top ldrne sp, =__stack_irq_cpu1_top cps #0x11 cmp r4, #0 ldreq sp, =__stack_fiq_cpu0_top ldrne sp, =__stack_fiq_cpu1_top cps #0x13 cmp r4, #0 ldreq sp, =__stack_svc_cpu0_top ldrne sp, =__stack_svc_cpu1_top cmp r4, #0 bne .Lsecondary_wait @ 裸机环境没有 C 运行库,启动代码负责清零 .bss。 ldr r0, =__bss_start ldr r1, =__bss_end mov r2, #0 .Lclear_bss: cmp r0, r1 strlo r2, [r0], #4 blo .Lclear_bss bl kernel_main .Lhalt: wfi b .Lhalt .Lsecondary_wait: @ CPU1不能和CPU0同时清.bss或建页表,先在无Cache状态等待发布。 ldr r0, =secondary_release ldr r2, =ZYNQ_SECONDARY_GO .Lsecondary_wait_loop: ldr r1, [r0] cmp r1, r2 beq .Lsecondary_go wfe b .Lsecondary_wait_loop .Lsecondary_go: bl secondary_kernel_main b .Lhalt .size _start, . - _start .macro FATAL_VECTOR name, number .align 2 .type \name, %function \name: mov r1, lr mrs r2, spsr mov r0, #\number b platform_exception_halt .size \name, . - \name .endm FATAL_VECTOR undefined_handler, 1 FATAL_VECTOR svc_handler, 2 FATAL_VECTOR prefetch_abort_handler, 3 FATAL_VECTOR data_abort_handler, 4 FATAL_VECTOR reserved_handler, 5 FATAL_VECTOR fiq_handler, 7 .align 2 .global irq_entry .type irq_entry, %function irq_entry: @ 修正返回PC,并把PC/CPSR压入被打断任务自己的SVC栈。 sub lr, lr, #4 srsdb sp!, #0x13 cps #0x13 @ r0-r12和任务自己的lr_svc也留在任务栈,共64字节私有现场。 stmdb sp!, {r0-r12, lr} mov r0, sp @ C handler切到CPU专用软件IRQ栈;禁止嵌套,所以每次从栈顶开始。 mrc p15, 0, r1, c0, c0, 5 and r1, r1, #0x3 cmp r1, #0 ldreq sp, =__stack_irq_cpu0_top ldrne sp, =__stack_irq_cpu1_top bl irq_dispatch @ irq_dispatch返回应恢复任务的私有frame,可与被打断任务不同。 mov sp, r0 ldmia sp!, {r0-r12, lr} rfefd sp! .size irq_entry, . - irq_entry .section .note.GNU-stack, "", %progbits
从规则走到实际实现 下面回到本章对应的小系统实现。这里不再假定读者已经打开源码,而是把关键地址、数据结构、指令序列和返回路径直接放在文章中解释。
第一版先写清装载条件 当前工程选择 DDR 中的 0x00100000 作为链接起点,预留 16 MiB 链接区域。这个地址是实验设计,不是 ARMv7-A 规定的复位入口。
QEMU 使用 -kernel zynq7020.elf 装载镜像;真实 Zynq 则有 BootROM、FSBL、可能的 U-Boot/JTAG 等前置阶段。真板进入本镜像前,至少要交代:
DDR 控制器和必要时钟是否已经初始化;
镜像真正装到哪个物理地址;
CPU 模式、端序、MMU/Cache 和向量选择状态;
UART 时钟、复位和 MIO/EMIO 引脚是否已设置;
CPU1 处于复位、ROM 等待还是软件等待状态。
现有 _start 没有实现通用的“清理任意 Bootloader 遗留状态”。例如它设置 VBAR,却没有把所有可能的 SCTLR 状态都规范化。QEMU 下运行成功,不能直接证明任意 FSBL 交接环境都能启动。
编译选项为什么要固定 基础版 CMake 使用的关键选项:
1 2 3 4 5 6 7 8 9 -mcpu=cortex-a9 -marm -mfloat-abi=soft -ffreestanding -fno-builtin -fno-stack-protector -ffunction-sections -fdata-sections -Wall -Wextra -Werror
它们分别约束目标 CPU、指令状态、浮点调用方式、裸机环境和代码生成假设。链接使用:
1 2 3 -nostdlib -nostartfiles -Wl,--gc-sections -T linker.ld
这意味着不能指望宿主操作系统的 C 运行时替代码初始化栈、清 BSS 或执行退出流程。代码若产生了编译器运行库调用,例如某些 64 位除法 helper,也必须显式提供对应实现或链接支持库;使用了 C 语言不代表这些支持会自动存在。
链接脚本建立的是地址合同 基础版 linker.ld 的开头:
1 2 3 4 5 6 7 8 9 10 11 12 OUTPUT_FORMAT("elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) MEMORY { RAM (rwx) : ORIGIN = 0x00100000, LENGTH = 16M } PHDRS { text PT_LOAD FLAGS(5); data PT_LOAD FLAGS(6); }
ENTRY 指定 ELF 的入口符号;MEMORY 用于链接布局和溢出检查;PHDRS 描述装载段。FLAGS(5) 是读加执行,FLAGS(6) 是读加写。
链接器里的 RAM(rwx) 不会配置 CPU 的 MMU 权限。真正的运行时只读、XN 和 Device 属性由后面的页表实现。ELF 段权限与页表权限可以互相印证,但不是同一个机制。
主要布局如下:
1 2 3 4 5 6 7 8 9 10 0x00100000 .vectors 32 字节对齐,保留异常向量 .text 启动与函数代码 .rodata 只读数据 → 对齐到 4 KiB 边界 .data 初始非零的可写对象 .bss 未初始化/零初始化对象 .stacks 每核、每模式的栈区 → 对齐到 16 KiB 边界 .translation_tables L1 16 KiB + coarse L2 1 KiB
代码与可写数据分到不同 4 KiB 页,是为了后面能实现 RO/X 与 RW/XN。如果一页里前半是指令、后半是可写全局变量,就无法只靠这一页的单一属性同时满足两种保护要求。
KEEP(*(.vectors)) 则防止向量段在 --gc-sections 下被当成没有引用的垃圾删掉。入口并不是通过普通 C 函数调用引用所有异常向量的。
BSS 为什么要清,data 为什么没有复制循环 BSS 中的 C 对象应以零值开始,但 NOLOAD 段通常不把同等大小的零字节放进文件。启动代码负责建立这个语言环境:
1 2 3 4 5 6 7 ldr r0, =__bss_start ldr r1, =__bss_end mov r2, #0 .Lclear_bss: cmp r0, r1 strlo r2, [r0], #4 blo .Lclear_bss
每次写入后 r0 增加 4。这个循环还依赖边界与长度满足按字清零的布局要求;扩展链接脚本时不能随意破坏这个条件。
当前工程的 ELF loader 把 .data 放在运行地址,VMA/LMA 没有设计成“Flash 保存、RAM 运行”的两套地址,所以没有传统单片机式的 .data 复制循环。
若后来改成从 Flash 执行、把 .data 复制到 DDR,就必须加入 LOADADDR、复制源和目的边界。不能因为当前 QEMU 代码没有复制循环,就认为所有裸机启动都不需要它。
向量表里放的是指令 1 2 3 4 5 6 7 8 9 10 11 .section .vectors, "ax", %progbits .align 5 _vectors: b _start b undefined_handler b svc_handler b prefetch_abort_handler b data_abort_handler b reserved_handler b irq_entry b fiq_handler
这八个槽位在 ARM 状态下各占 4 字节。IRQ 槽位偏移为 0x18。与 Cortex-M 常见的向量地址数组不同,这里放的是入口指令。
向量表放在内存中还不够,CPU 必须选择它:
1 2 3 4 ldr r0, =_vectors mcr p15, 0, r0, c12, c0, 0 dsb isb
这设置 VBAR。前提是当前安全状态和高向量选择等控制条件与此方案相容。后续一旦切换页表,向量所在虚拟地址也必须保持可执行、可访问。
每核、每模式的 SP 都要有归属 启动先屏蔽 IRQ/FIQ,再读 MPIDR.Aff0 分流。代码为 UND、ABT、IRQ、FIQ、SVC 分别初始化 banked SP,而且每个模式都分 CPU0 与 CPU1 两份内存。
基础版预留的模式栈大小为:
模式
每核大小
原因
UND
1 KiB
未定义异常诊断
ABT
1 KiB
Abort 诊断
IRQ
4 KiB
独立 C 中断处理栈区
FIQ
1 KiB
当前未实现专门 FIQ 业务
SVC
16 KiB
启动和内核执行
任务另有独立数组作为自己的 SVC 运行栈。后续切换到任务时,SP 会离开启动 SVC 栈,进入对应任务栈。
这个布局不意味着 FIQ 功能已实现;分配一个栈,只是为异常诊断和后续扩展提供起点。
CPU1 不能跟着 CPU0 清零共享状态 两个核可能进入同一个 _start。设置各自的模式栈之后,CPU1 进入等待路径,CPU0 才清 BSS、建立共享结构并进入主初始化。
1 2 3 cmp r4, #0 bne .Lsecondary_wait @ 只有 CPU0 执行 BSS 清零与 kernel_main
否则 CPU0 正在更新的任务控制块,可能被稍后到达的 CPU1 再次清零。这个错误表现为随机丢状态,根因却发生在很早的启动阶段。
release 变量放在显式初始化的数据区,并通过特定 magic 值判断,避免把尚未初始化的 BSS 当成可靠的核间协议。完整发布顺序留到多核篇分析。
UART 是最早可依赖的观察通道 src/uart.c 使用 Zynq PS UART0:
1 2 3 4 5 6 #define ZYNQ_UART0_BASE 0xE0000000u static volatile uint32_t *uart_register (uint32_t offset) { return (volatile uint32_t *)(uintptr_t )(ZYNQ_UART0_BASE + offset); }
初始化配置控制寄存器、8N1 模式、关闭 UART 中断并配置波特率;发送函数轮询 TX FIFO 是否满,再写 FIFO。
1 2 3 4 while ((*uart_register(UART_CHANNEL_STATUS) & UART_CHANNEL_STATUS_TX_FULL) != 0u ) { } *uart_register(UART_FIFO) = (uint32_t )(uint8_t )ch;
volatile 让这些 MMIO 访问作为实际访问发出,不把轮询优化成读一次寄存器。它不替代 Device 内存属性,也不自动完成 Cache 一致性或核间同步。
现有波特率参数采用 QEMU 模型的参考时钟假设。真板先核对 UART 的实际输入时钟,再套用分频公式;乱码首先检查时钟和分频,不要直接修改字符串输出函数。
建好镜像后先看 ELF,再运行 在任一小系统根目录:
1 2 3 4 5 ./scripts/build.sh arm-none-eabi-readelf -h build/zynq7020.elf arm-none-eabi-readelf -lW build/zynq7020.elf arm-none-eabi-nm -n build/zynq7020.elf arm-none-eabi-objdump -d build/zynq7020.elf
重点核对入口是不是 _start、LOAD 地址是否正确、是否存在 RX/RW 两类装载段、各栈是否重叠、向量是否真的留下、页表是否按要求对齐。
随后运行项目脚本,其核心命令为:
1 qemu-system-arm -M xilinx-zynq-a9 -cpu cortex-a9 -smp 2 -display none -monitor none -serial stdio -kernel build/zynq7020.elf
这组参数不提供像素显示设备。终端彩色面板是 UART 上的 ANSI 控制序列,不代表有 framebuffer。平台支持范围可查 QEMU 的 Zynq 板文档 。
当能看到 BSS/data 检查和 “CPU0 reached C entry” 时,启动链已经具备后续实验的基础。若这里尚不稳定,先停留在 ELF、栈、装载与 UART,不必同时引入定时器和调度。
下一篇:线程与协同上下文切换 。
完整交叉构建配置 编译选项决定指令集、ABI 和链接方式。这里保留完整配置,便于把前面的概念与最终命令对应起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 cmake_minimum_required (VERSION 3.20 )project (zynq7020_port LANGUAGES C ASM)set (LINKER_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/linker.ld" )set (ZYNQ_GDB_PORT 1234 CACHE STRING "QEMU GDB 远程调试端口" )option (ZYNQ_CONSOLE_ANSI "启用彩色 ANSI 串口启动页与实时仪表盘" ON )add_executable (zynq7020.elf src/start.S src/context_switch.S src/context.c src/console.c src/global_timer.c src/cache.c src/irq.c src/kernel.c src/mmu.c src/scheduler.c src/smp.c src/uart.c )target_include_directories (zynq7020.elf PRIVATE include )if (ZYNQ_CONSOLE_ANSI) target_compile_definitions (zynq7020.elf PRIVATE ZYNQ_CONSOLE_ANSI=1 )else () target_compile_definitions (zynq7020.elf PRIVATE ZYNQ_CONSOLE_ANSI=0 )endif ()target_compile_options (zynq7020.elf PRIVATE -mcpu=cortex-a9 -marm -mfloat-abi=soft -ffreestanding -fno-builtin -fno-stack-protector -ffunction-sections -fdata-sections -Wa,--noexecstack -Wall -Wextra -Werror $<$<COMPILE_LANGUAGE:C>:-std=c11> $<$<CONFIG:Debug>:-O0> $<$<CONFIG:Debug>:-g3> )target_link_options (zynq7020.elf PRIVATE -mcpu=cortex-a9 -marm -mfloat-abi=soft -nostdlib -nostartfiles -Wl,--build-id=none -Wl,-z,noexecstack -Wl,--gc-sections -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR} /zynq7020.map -T${LINKER_SCRIPT} )set_property (TARGET zynq7020.elf APPEND PROPERTY LINK_DEPENDS "${LINKER_SCRIPT}" )add_custom_command (TARGET zynq7020.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:zynq7020.elf> ${CMAKE_CURRENT_BINARY_DIR} /zynq7020.bin COMMAND ${CMAKE_SIZE} $<TARGET_FILE:zynq7020.elf> COMMENT "Generating raw image and printing image size" )configure_file ( cmake/zynq7020.gdb.in ${CMAKE_CURRENT_BINARY_DIR} /zynq7020.gdb @ONLY )add_custom_target (qemu-debug COMMAND ${CMAKE_CURRENT_SOURCE_DIR} /scripts/run_qemu_debug.sh ${ZYNQ_GDB_PORT} $<TARGET_FILE:zynq7020.elf> DEPENDS zynq7020.elf USES_TERMINAL COMMENT "Starting paused QEMU GDB server on port ${ZYNQ_GDB_PORT}" )add_custom_target (gdb-connect COMMAND ${CMAKE_GDB} -x ${CMAKE_CURRENT_BINARY_DIR} /zynq7020.gdb DEPENDS zynq7020.elf USES_TERMINAL COMMENT "Connecting arm-none-eabi-gdb to localhost:${ZYNQ_GDB_PORT}" )
完整链接脚本 链接脚本是启动汇编的地址来源。阅读时重点对照向量表、代码段、可写段、转换表和各模式栈的边界符号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 OUTPUT_FORMAT("elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) MEMORY { /* QEMU/当前练习从 DDR 0x00100000 加载;真板需按 FSBL/U-Boot 交接复核。 */ RAM (rwx) : ORIGIN = 0x00100000, LENGTH = 16M } PHDRS { text PT_LOAD FLAGS(5); data PT_LOAD FLAGS(6); } SECTIONS { . = ORIGIN(RAM); __image_start = .; .vectors ALIGN(32) : { __code_start = .; KEEP(*(.vectors)) } > RAM :text .text ALIGN(4) : { *(.text.startup) *(.text .text.*) } > RAM :text .rodata ALIGN(4) : { *(.rodata .rodata.*) __code_end = .; } > RAM :text /* 代码与可写数据分开页,MMU 才能真正设置 RO/X 与 RW/XN。 */ .data ALIGN(4K) : { __data_start = .; *(.data .data.*) __data_end = .; } > RAM :data .bss ALIGN(8) (NOLOAD) : { __bss_start = .; *(.bss .bss.*) *(COMMON) __bss_end = .; } > RAM :data .stacks ALIGN(8) (NOLOAD) : { /* 每核都必须拥有不同的 banked SP,尤其不能共享 IRQ/SVC 栈。 */ __stack_und_cpu0_bottom = .; . += 1K; __stack_und_cpu0_top = .; __stack_und_cpu1_bottom = .; . += 1K; __stack_und_cpu1_top = .; __stack_abt_cpu0_bottom = .; . += 1K; __stack_abt_cpu0_top = .; __stack_abt_cpu1_bottom = .; . += 1K; __stack_abt_cpu1_top = .; __stack_irq_cpu0_bottom = .; . += 4K; __stack_irq_cpu0_top = .; __stack_irq_cpu1_bottom = .; . += 4K; __stack_irq_cpu1_top = .; __stack_fiq_cpu0_bottom = .; . += 1K; __stack_fiq_cpu0_top = .; __stack_fiq_cpu1_bottom = .; . += 1K; __stack_fiq_cpu1_top = .; __stack_svc_cpu0_bottom = .; . += 16K; __stack_svc_cpu0_top = .; __stack_svc_cpu1_bottom = .; . += 16K; __stack_svc_cpu1_top = .; /* 兼容既有 CPU0 监测符号。 */ __stack_irq_bottom = __stack_irq_cpu0_bottom; __stack_irq_top = __stack_irq_cpu0_top; } > RAM :data .translation_tables ALIGN(16K) (NOLOAD) : { __translation_table = .; . += 16K; __translation_table_end = .; __image_page_table = .; . += 1K; __image_page_table_end = .; } > RAM :data __image_end = .; /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) } } ASSERT(__image_end <= ORIGIN(RAM) + LENGTH(RAM), "Zynq image exceeds RAM region")
重点回看 启动代码把链接脚本中的地址合同变成处理器状态:设置 VBAR、分配每核每模式栈、由 CPU0 清 BSS,并在满足 ABI 后进入 C。CPU1 必须等待共享初始化完成。第一行 UART 输出之前,每一步都应能由 ELF、反汇编或 GDB 验证。
本章总结 启动代码把链接脚本中的地址合同变成处理器状态:设置 VBAR、分配每核每模式栈、由 CPU0 清 BSS,并在满足 ABI 后进入 C。CPU1 必须等待共享初始化完成。第一行 UART 输出之前,每一步都应能由 ELF、反汇编或 GDB 验证。
下一章在无中断条件下实现两个任务的协同切换,先验证最小普通上下文。