ARMv7-A小系统实践(十二):用QEMU和GDB把整条实现链验清楚
本章记录什么
最后一章不再增加新的架构功能,而是把整条系统路径重新走一遍,并建立一套可复用的验证与排错方法。目标是让“能够运行”变成一组可以复查的证据:ELF 段与符号正确,启动输出按阶段出现,任务切换有双向进展,Timer IRQ 能重复触发,设备清源与 EOI 都发生,MMU/Cache 状态可读,CPU1 能完成 SGI 往返。
在这一章之前做了什么
前十章已经依次建立处理器状态、ABI、启动、协同上下文、异常现场、GIC/Timer、抢占、MMU、Cache 和双核底座。系统越完整,故障越不适合靠“多加几行打印”猜测,因为串口本身可能不可重入,读取 IAR 还有副作用,Cache 又可能让共享状态的观察滞后。
本章按照静态检查、分层断点、寄存器观察、串口证据和已知 Bug 五条线组织。文章末尾给出能力边界,避免把教学闭环夸大成完整产品操作系统。
系列目录 · 上一篇:双核启动、每核资源与 SGI
本章的阅读顺序
先读基础概念,确认每个术语解决什么问题;再看设计推导,理解实现为什么采用这种保存集合、初始化顺序或状态机;随后逐段阅读代码和执行轨迹;最后用验证方法与错误案例检查理解。文中保留寄存器名、指令名和标准缩写,叙述部分统一使用简体中文。
从真实问题积累可复用的排错规则
这一部分按系统能力形成的先后顺序记录问题。每条记录都包含现象、原因、修正和可复查证据。路径与提交历史不是重点,真正需要保留的是判断方法:故障属于构建、启动、ABI、异常、GIC、设备还是调度层。
1. 总体阶段
1 | |
2. 2026-08-02:第一批代码
本节目标
在不引入调度和中断变量的前提下,先证明以下链路成立:
1 | |
已确定的移植边界
- x86_64 基线与 RTEMS 参考代码保持只读,避免在对照材料中混入实现改动。
- ARM 平台层单独组织,避免把 x86_64 与 ARM 的启动、异常和中断实现混写。
- 先保留 x64 demo 的行为语义,再替换启动、上下文、中断控制器和时钟等架构实现。
- 第一阶段使用 QEMU
xilinx-zynq-a9和 CPU0;真板差异作为后续 BSP 参数处理。
第一批文件
CMakeLists.txt:ARM 裸机目标和 ELF/BIN 产物。cmake/arm-none-eabi-toolchain.cmake:本机 ARM GNU 工具链定位。linker.ld:0x00100000链接布局、.bss和异常模式栈。src/start.S:向量表、VBAR、模式栈、.bss清零与 C 入口。src/uart.c:Zynq PS UART0 轮询输出。src/kernel.c:启动自检与调试证据。
第一批验证结果
ARM GNU Toolchain 15.2.1 构建成功,QEMU 输出:
1 | |
3. 2026-08-02:普通任务上下文
新增 Context_Control、_CPU_Context_Initialize()、_CPU_Context_switch()、_CPU_Context_restore() 和新任务 trampoline。
普通上下文保存集合为:
1 | |
新任务用 r4/r5/r6 携带入口、参数和退出入口。C 与汇编使用共同偏移宏,并由_Static_assert 验证结构偏移,防止修改 C 结构后汇编静默使用旧偏移。
QEMU 已验证:
1 | |
这不是“打印看起来像切换”,而是 A、B 和 boot 三套独立 SP/LR/寄存器现场均被真实恢复。
4. 2026-08-02:IRQ、GIC 与 Private Timer
新增 64 字节 CPU_Interrupt_frame。IRQ 汇编入口保存 r0-r12、LR_irq、SPSR_irq,
保持调用 C 时 SP 八字节对齐,返回时用 subs pc, lr, #4 同时恢复 PC 与 CPSR。
第一版 GIC/Timer 配置:
- Private Peripheral Base:
0xF8F00000; - GICC:
0xF8F00100; - Private Timer:
0xF8F00600; - GICD:
0xF8F01000; - ID29:
GICD_ISENABLER0.bit29; - handler 顺序:
IAR → ID29 → PT_STATUS=1 → tick++ → EOIR=iar。
QEMU 已连续验证三次:
1 | |
连续进入证明设备清源与 GIC EOI 都正确;缺少任意一项都不会得到这条稳定链路。
5. 2026-08-06:独立 IRQ 栈与远程调试
原理文档给出的约束
《操作系统移植原理》的核心不是“再分配一块数组”这么简单,而是:中断处理使用
专用栈时,抢占调度前必须处理好“IRQ 异常帧属于哪个任务”。当前实现先完成可独立
验收的前半部分:
1 | |
增加的运行时证据:
- 栈底 4 个
0x49525147哨兵字; - handler 内最低 SP;
- 最近一次
CPU_Interrupt_frame地址; - SP/frame 范围及哨兵完整性检查。
QEMU 实测:
1 | |
所以本节证明了“独立栈已经分配、硬件确实选中它、汇编和 C handler 都在使用它”。
当时在抢占切换前保留的设计边界
当时 IRQ 栈由 CPU0 共享,而不是每个任务一份。若直接在 irq_dispatch() 中调用已有
普通 _CPU_Context_switch(),旧任务的异常帧会长期留在共享 IRQ 栈;下一任务再次
中断时可能覆盖它,而且普通 Context 也没有保存异步现场所需的 r0-r3/r12/返回 PC。
最终轮已经按这一约束改成“任务私有 frame + 每核独立 C IRQ 软件栈”,详见 8.3。
第一版仍禁止中断嵌套,避免同时引入嵌套层级和共享栈所有权。
Eclipse/QEMU 调试链
- 另一套 ARMv7-A 工程的 QEMU 脚本使用
-s,即默认 GDB TCP 1234; - 本原型显式使用
-S -gdb tcp::1234,保证 CPU 在第一条指令前暂停; - CMake 新增
qemu-debug与gdb-connect,并生成build/zynq7020.gdb; - 实测 GDB 停在
_start的0x00100020,成功读取 PC、CPSR、SP 和启动反汇编。
6. Bug 记录
BUG-001:ELF 出现 executable stack 警告
- 现象:C 对象缺少
.note.GNU-stack,链接器推断可执行栈。 - 修复:编译加入
-Wa,--noexecstack,链接加入-Wl,-z,noexecstack。 - 验证:重新链接不再出现警告。
BUG-002:ELF 出现 RWX LOAD segment 警告
- 现象:单一
RAM (rwx)使输出 LOAD segment 同时可读、写、执行。 - 修复:链接脚本增加
PHDRS,把向量/代码/只读数据映射到R E,把 data/bss/stack 映射到RW。 - 验证:
readelf -l显示两个段分别为R E与RW。
BUG-003:在构建子目录执行 Git 根路径命令
- 现象:第一步 ARM 骨架已经构建成功,但在
ports/zynq7020中执行git add .gitignore ports/zynq7020,Git 提示 pathspec 不存在。 - 原因:命令参数是相对 Git 仓库根目录写的,当前 shell 却位于 port 子目录。
- 修复:回到实际版本库根目录再执行版本控制命令。
- 结论:执行 Git 操作前先确认
git rev-parse --show-toplevel和当前pwd。
BUG-004:保留时间戳复制导致 Ninja 使用旧对象
- 现象:最终 Timer 文件已经与
60一致,但 Ninja 输出no work to do,QEMU 仍运行
上一步 ELF,只显示上下文测试,没有 ID29。 - 原因:
cp -a保留了源文件时间戳;复制后的源码时间不晚于已有对象,增量构建系统
判断无需重编译。 - 修复:执行
cmake --build build --clean-first,重新编译全部 7 个对象。 - 验证:ELF text 从 2933 增至 4013 字节,QEMU 连续输出三次 ID29。
- 结论:重放历史、切换 commit、批量复制或修改系统时间后,关键验收必须 clean build;
不能只相信ninja: no work to do。
BUG-005:zsh 中使用 path 变量破坏命令搜索路径
- 现象:逐文件哈希验证
61时,第一轮以后连续出现git: command not found。 - 原因:zsh 的数组变量
path与环境变量PATH绑定;循环把文件名读入path,等于
覆盖命令搜索路径。 - 修复:循环变量改为
file_name,哈希复验结果为65/65,mismatch=0。 - 结论:shell 脚本变量不要使用
path、status、commands等可能具有特殊含义的名称;
使用带任务语义的名称,例如source_file、build_dir、toolchain_prefix。
DEBUG-001:GDB detach 后 QEMU 会继续运行
- 现象:批处理 GDB 在
_start读取寄存器后执行detach,QEMU 随即继续执行并输出完整验收日志。 - 原因:detach 表示调试器脱离目标,不等于让远端 CPU 继续保持暂停。
- 结论:Eclipse 调试时不要用 detach 代替暂停;使用断点/暂停按钮控制执行。需要结束
调试时同时停止 Eclipse 会话和qemu-debug终端。
当前未解决项
- 尚未验证真板 FSBL/U-Boot/JTAG 的入口、DDR 和 UART 时钟交接。
- 尚未实现完整 SMP 调度、任务迁移、锁和 TLB shootdown。
- 尚未接入 DMA,因此 Cache 一致性 API 还没有真实 DMA 设备回归。
7. 关键架构结论
7.1 为什么先只做 CPU0
启动、上下文和 IRQ 三条链在单核下即可分别验证。直接加入 CPU1 会同时引入共享
Distributor、每核 CPU Interface、每核 PPI 状态和次核释放流程,不利于定位第一阶段错误。
7.2 为什么先不开 IRQ
如果 .bss、VBAR、IRQ 栈、UART 和链接地址尚未独立验证,一开 IRQ 后的沉默或崩溃
无法区分是启动问题、异常帧问题、GIC 问题还是 Timer 清源问题。
7.3 x64 中哪些语义会保留
Context_Control表示一个可恢复的普通任务现场;_CPU_Context_Initialize()构造新任务第一次运行的现场;_CPU_Context_switch()完成 A→B→A;- Timer ISR 更新 tick,并在 IRQ 尾部决定是否调度。
需要替换的是寄存器集合、汇编指令、异常入口、APIC/IDT 和时钟硬件。
7.4 普通上下文和 IRQ 帧已经在代码中分开
- 普通同步切换依赖 AAPCS,只保存跨函数调用必须保持的主干寄存器。
- IRQ 可发生在任意指令位置,因此入口保存全部共享通用寄存器和 SPSR/LR 返回信息。
- 最终抢占切换已在 IRQ 尾部用任务私有完整 frame 连接,仍没有把 IRQ 帧当普通上下文。
7.5 独立 IRQ 栈的多核含义
早期版本的静态 sp_irq 只服务 QEMU CPU0。最终版本已为 CPU0/CPU1 分配不同的
banked sp_irq 与 4 KiB IRQ 软件栈;两个核没有把 SP 指向同一块内存。
8. 2026-08-06:最终移植轮完成
8.1 通用 IRQ 与 Fault 诊断
ID29 不再由分发器硬编码。irq_table[id] 保存 handler 与 argument,Private Timer 和
SGI0 都通过注册接口接入。主干固定为:
1 | |
Abort 诊断除 vector、LR、SPSR 外,额外读取 DFSR/DFAR/IFSR/IFAR,便于区分权限、
翻译和外部访问故障。
8.2 MMU 与 Cache
新增 16 KiB L1 translation table 和 1 KiB L2 coarse table。采用 identity map,
但落实了权限与类型:
- 镜像代码只读、可执行;
- data/bss/stack/page table 可写、XN;
- DDR 为 Normal WBWA/Shareable;
- UART、SLCR、private peripherals 与 L2C-310 为 Device/Shareable/XN;
0x70000000保持 unmapped,PAR 查询应报告 fault。
CPU0 依次启用 MMU、SCU、ACTLR.SMP、L1 I/D Cache 与 L2C-310;CPU1 使用同一页表,
但独立设置 TTBR/TTBCR/DACR/SCTLR、L1 和 ACTLR.SMP。QEMU 实测:
1 | |
8.3 抢占现场所有权
最终没有把异步 frame 长期放在共享 IRQ 栈:
1 | |
Timer handler 只做清源、tick++ 和 scheduler_request();选任务发生在完整 IAR/EOIR
处理后的最外层 IRQ tail。busy A/B 都没有普通 _CPU_Context_switch() 调用。
1 | |
8.4 双核底座
_start 用 MPIDR.Aff0 选择 CPU0/CPU1 的 UND/ABT/IRQ/FIQ/SVC 栈。CPU1 在 Cache
关闭状态等待 secondary_release,CPU0 完成共享初始化后 clean release flag、写
Zynq 0xfffffff0 kick address 并 SEV。CPU1 随后初始化自己的 MMU/Cache/GICC。
GIC Distributor 仅由 CPU0 初始化;GICC、SGI/PPI banked enable 和 IRQ 软件栈按核
初始化。SGI0 实测 CPU0→CPU1→CPU0:
1 | |
这证明 CPU1 启动、共享状态可见性、per-CPU 栈和 IPI 通路;不等于已实现两个 CPU
共同运行通用任务的 SMP scheduler。
BUG-006:误把任意中断点的任务 SP 强制要求为 8 字节对齐
- 现象:抢占任务能够轮转,但 IRQ frame 边界检查偶发失败。
- 错误判断:认为 IRQ 到达时任务 SP 必须始终保持 AAPCS 的 8 字节对齐。
- 根因:AAPCS 要求 public interface/函数调用边界对齐;IRQ 可以打断函数内部,
此时编译器可能临时把 SP 调整为 4 字节对齐。 - 修复:frame 只检查非空和恢复合同,不再把异步中断点的 SP 低三位当错误。
- 结论:同步 ABI 边界规则不能机械套到任意异步采样点。
8.5 最终验证证据
cmake --build build -j 4在-Werror下通过;- 双核 QEMU 全部阶段输出 PASS;
- GDB 1234 的
info threads同时看到 CPU0/CPU1,并在kernel_main断点停住; readelf -lW只有R E和RWLOAD segment,无 W+X;objdump的irq_entry实际包含srsdb、cps、rfeia;- 每核模式栈符号互不重叠,L1 表位于
0x00114000且 16 KiB 对齐。
9. 最终边界
QEMU 上已经完成 x64 mini 关键平台原理的 ARM 替换,并额外完成真实 Timer 抢占、
Cache/L2 和 Cortex-A9 双核 SGI 底座。仍需如实标记为未完成:真板启动与时钟交接、
完整 SMP 调度、DMA 一致性实测、VFP/NEON、用户态、动态内存和通用内核子系统。
11. 2026-08-11:补齐永久双任务协同切换
对原 x86 行为的判断
原 x86 task1() 和 task2() 都在死循环里显式调用 _CPU_Context_switch()。哪个任务
运行、何时让出 CPU,由任务自己决定,而不是由 Timer 中断强行打断,因此它是最小的
协同式切换示例。原代码虽然初始化了时钟,但这条 A/B 切换链本身不依赖时钟调度。
ARM 实现
- 保留原有
boot → A → B → A → boot有限自检,避免永久任务挡住后续验收; - 保留 Timer 驱动的 busy A/B 抢占测试,用来区分同步 context 与异步 IRQ frame;
- 所有有限验收完成后,重新初始化两份普通任务栈和
Context_Control; - cooperative A/B 各自递增一个全局计数、输出一次,再主动切到另一任务;
- 使用
_CPU_Context_restore()启动第一个常驻任务,之后永不回到 boot context。
1 | |
关键结论与边界
这不是“两个结构体互换”。context_switch.S 用 STM/LDM 和独立的 STR/LDR 保存、
恢复 r4-r11、sp、lr、cpsr,更换 SP 后就进入另一任务的调用栈;切回来会从原_CPU_Context_switch() 调用之后继续。
当前闭环证明了两个独立任务能长期保存现场并协作让出 CPU,但还没有通用任务队列、
READY/BLOCKED 状态、阻塞与唤醒、优先级、任务退出和资源回收,不能写成完整调度器。
验证:ARM GNU clean build 通过;双核 QEMU 原有 MMU/Cache/IRQ/SMP/抢占 PASS 均保留,
脚本额外检查前 8 条协同输出严格满足 A1、B1、A2、B2、A3、B3、A4、B4。
12. 2026-08-11:ANSI 串口启动页与运行仪表盘
为什么没有直接复制 x86 framebuffer
原 x86 界面依赖 GRUB Multiboot2 提供的 1024×768×32 framebuffer,然后使用 8×16
点阵字库逐像素绘制绿色字符。当前 QEMU xilinx-zynq-a9 只接 PS UART,不能直接挂ramfb;对照的 Zynq-7020 BSP 中也没有发现 HDMI/VGA、AXI VDMA 或 VTC 显示
通路。因此能复用的是“console 与后端分离”的设计,不是 framebuffer 地址和 GRUB 合同。
当前实现
- 新增
console.h/console.c,显示逻辑不再散落在协同任务中; - 启动时清屏并显示 ASTRA/Zynq-7020 ARMv7-A 彩色标题;
- 所有架构自检仍按原顺序输出,失败证据不会被跳过;
- 自检结束后进入固定位置仪表盘,隐藏光标并原地刷新,不再滚屏;
- 面板显示 CPU0/CPU1、MMU、L1/L2、SCU、GIC、IRQ 状态;
- 面板显示 A/B 计数、RUNNING/READY、协同 yield 次数和最近一次切换事件;
ZYNQ_CONSOLE_ANSI=OFF可以编译纯文本回退,适合日志文件和不支持 ANSI 的串口工具。
顺手修复的状态机问题
此前 preempt_finish() 即使打印抢占验收失败,也会继续进入协同任务,可能用后续动态
输出掩盖真正失败。本节改成失败后立即停机,只有抢占验收成功才能打开运行仪表盘。
验证
- ANSI ON:clean build 通过,全部旧 PASS 保留;检测到标题、A/B 非零计数、yield 计数;
- 从原始 UART 字节流提取最近事件,前八次严格为
ABABABAB; - ANSI OFF:clean build 通过,纯文本仍严格输出
A1、B1、A2、B2……; - 最终重新构建为 ANSI ON,作为默认演示配置。
准确边界:这仍是串口字符终端 UI,不是真正的像素 framebuffer。真板图形输出必须先有
PL 侧显示 bitstream、像素时钟、VDMA/VTC 和 HDMI/VGA 物理通路,再实现 framebuffer
后端以及 DMA/Cache 一致性处理。
13. 2026-08-11:运行仪表盘 v2——真正可观察的动态输出
BUG-007:把已停止的 Private Timer 历史值当作运行 tick
- 现象:仪表盘底部
Timer ticks永远不变,容易被理解为系统没有继续运行; - 根因:抢占自检结束时,
preempt_finish()会关闭 IRQ 并停止 Private Timer,之后才进入
永久协同任务。这个数是抢占自检的验收结果,不是运行时钟; - 不能直接修成“重新打开 Private Timer”:抢占 scheduler 仍保存着测试任务的 frame,重新
产生 PPI29 可能让 IRQ tail 切回已经结束用途的抢占测试上下文; - 修复:底部改名为
Private Timer ticks [HISTORY/STOPPED],另启 Cortex-A9 Global Timer,
仅轮询 counter、不启 comparator、不启 IRQ,作为不会影响调度路径的实时证据。
BUG-008:固定频率采样总是撞到任务 A
A/B 每次切换都会把总 yield 加一。如果每隔 4096(偶数)次切换刷新,采样相位不变,
终端就会一直显示 A,造成 B 没有运行的错觉。v2 将 Global Timer 差值作为主限帧条件,
并使用奇数次切换作为仿真兜底;滚动窗口中可同时观察 A、B 的运行样本。
v2 可观察项
- A/B 独立计数、RUNNING/READY、动态 activity bar;
- 当前任务、下一任务、总 cooperative yields;
- 持续递增的 Global Timer counter、UI heartbeat 旋转符、heartbeat 帧号;
- 每次界面采样跨过的 yield 数,以及最近 5 条任务运行样本;
- 启动自检历史与实时运行状态分区显示,不再混淆。
验证:ANSI 和纯文本配置都在 -Werror 下编译通过;QEMU 6 秒运行中 Global Timer、
UI heartbeat、A/B count 和 yield 持续增长,最近输出区同时出现任务 A 与任务 B。
构建、运行与远程调试
7. 构建与运行
需要 cmake、ninja、arm-none-eabi-gcc 和 qemu-system-arm:
1 | |
默认启用 ANSI 彩色仪表盘。串口工具不支持 ANSI、需要保存纯文本日志或做自动化测试时:
1 | |
恢复仪表盘只需重新执行 ZYNQ_CONSOLE_ANSI=ON ./scripts/build.sh。该选项只改变显示层,
不会改变上下文、IRQ、MMU、Cache 或 SMP 实现。
主要产物:
build/zynq7020.elf:带源码符号,用于 QEMU/GDB;build/zynq7020.bin:裸镜像,真板加载方式另行确定;build/zynq7020.map:检查段、页表和每核栈地址。
如果源码来自批量复制、历史重放或系统时间变化,使用 clean build:
1 | |
8. GDB/Eclipse 远程调试
终端一:
1 | |
终端二:
1 | |
Eclipse 使用 GDB Hardware Debugging:
| 字段 | 值 |
|---|---|
| Application | build/zynq7020.elf |
| GDB | arm-none-eabi-gdb |
| Connection | TCP localhost:1234 |
| Breakpoint | _start 或 kernel_main |
-S 会让两个 CPU 在第一条指令前暂停。实测 GDB info threads 能看到 CPU0/CPU1,
在 kernel_main 断住时 CPU1 位于 start.S 的 secondary wait。
最终验收证据与实现边界
9. 最终验收基准
2026-08-11 的关键输出:
1 | |
静态检查同时满足:
- ELF 只有
R E与RW两个 LOAD segment,没有 W+X; irq_entry反汇编包含SRSDB、CPS、RFE;- 每个 CPU 的各模式栈地址互不重叠;
- GDB 端口 1234 可连接并识别两颗 CPU。
10. 已完成与未完成的准确边界
与原始 x64 mini 相比,已替换/复用并形成运行闭环的核心功能包括启动、页表/MMU、
内存属性、普通上下文、中断分发、Timer 和调试入口;ARM 版本还额外实现了真实 Timer
抢占、L1/L2 管理、CPU1 启动和 SGI,并保留了原 x86 双任务永久协同运行的行为语义。
仍未完成:
- 真板 FSBL/U-Boot/JTAG 启动、时钟与 DDR 交接实测;
- 完整 SMP 调度、任务迁移、spinlock/原子同步和 TLB shootdown;
- 通用协同调度器的任务队列、状态机、阻塞/唤醒、优先级和退出回收;
- VFP/NEON 上下文、用户态/系统调用、进程地址空间;
- 动态物理内存分配、heap、VFS、网络和通用驱动框架;
- DMA cache 一致性、嵌套 IRQ、系统级 fault recovery;
- 产品化异常转储、压力测试和真实硬件回归。
- 真 framebuffer/HDMI/VGA 输出;当前界面是 UART 上的 ANSI 控制序列。
因此可以说“x64 mini 的关键平台原理已经在 ARMv7 Cortex-A9 上完成可运行替换,并
建立了 Zynq-7020 双核底座”,不能说“完整操作系统或完整 SMP 已经移植完成”。
从规则走到实际实现
下面回到本章对应的小系统实现。这里不再假定读者已经打开源码,而是把关键地址、数据结构、指令序列和返回路径直接放在文章中解释。
重走一次完整执行链
1 | |
这种顺序不是唯一设计,但每个后续阶段都依赖前面的基本条件。比如 IRQ handler 调 C,依赖 AAPCS 和正确栈;CPU1 读共享状态,依赖可见性和初始化顺序;Timer 抢占,依赖足够完整的任务现场。
两版程序怎样复现
基础版:
1 | |
精简版:
1 | |
两组命令应从各自工程根目录执行。精简版没有基础版的 ANSI console 后端,不要把界面开关当成所有目录都支持的统一接口。
脚本配置 Debug、Cortex-A9、ARM 状态、soft-float。若复制工程后 CMake 报源码路径不匹配,使用新的构建目录,或按项目管理方式重新生成缓存;直接搬旧 build 目录不能保证它引用的是当前源码。
修改编译器优化级别也要检查最后真正生效的命令。当前 CMake 对 Debug 添加 -O0,不能只在外部追加一个 -O2 就假定所有目标均已优化。
本次整理实际观察到的输出
基础版在独立构建目录重新编译后,双核 QEMU 保留了以下关键结果:
1 | |
随后纯文本输出:
1 | |
精简版观察到:
1 | |
计数与交错会随仿真速度变化,不应要求每次逐字相同。精简版的输出同时证明任务有进展、Timer 请求出现、不切换 IRQ 返回路径被使用;日志中的 cooperative_switches 是总切换计数,不能当作纯协同次数。
这些是 QEMU 运行证据。PASS 字符串的文字范围也可能大于它实际检查的条件,例如当前 MMU 自检主要检查使能与翻译,不等于已经用故障注入验证全部 AP/XN 权限。
如何安全结束持续运行的实验
普通脚本使用 monitor none 与 serial stdio,没有配置 monitor/serial 的复用控制台。不要默认所有环境都支持 Ctrl-A X 这一退出组合。
命令行批量运行可以使用 timeout,并解释预期的退出状态:
1 | |
对一个设计为永久任务循环的程序,timeout 的 124 表示达到观察时间后停止,不是内核主动报告失败。交互终端中可使用对应终端的中断方式结束当前 QEMU。
GDB 从第一条指令开始
项目提供默认端口 1234 的调试入口:
1 | |
另一个终端:
1 | |
1 | |
也可使用项目生成的 build/zynq7020.gdb 或 gdb-connect 目标。先检查是否已有进程占用 1234,避免连接到上一轮尚未关闭的仿真器。
对 Eclipse Remote Debug,关键参数仍是同一个 ELF、交叉 GDB、主机与端口。IDE 不能修复错误 ELF 或错误符号路径;断点映射到错误源码时先核对构建产物。
GDB 的 detach 通常会让目标继续执行。需要研究暂停时的现场,先明确 detach、continue、interrupt 和退出客户端的不同效果。
每一层都有自己的观察点
| 位置 | 观察对象 | 要回答的问题 |
|---|---|---|
| _start | PC、CPSR、MPIDR、各模式 SP | 是否进入预期核与模式 |
| kernel_main | BSS/data probe、UART | C 运行环境是否建立 |
| _CPU_Context_switch | r0/r1、旧/新 SP/LR | 是否真的更换任务调用栈 |
| irq_entry 首指令 | LR_irq、SPSR_irq | 硬件交付的返回状态是什么 |
| irq_dispatch | frame、当前 SP | frame 在任务栈,C SP 在中断栈吗 |
| IRQ tail | current_task、need_reschedule | 谁被选中,为什么 |
| RFE 前 | frame 中 PC/CPSR | 即将恢复的是哪一个执行流 |
| MMU 初始化后 | TTBR、表项、PAR | 映射与属性是否匹配 |
| secondary_kernel_main | CPU1 SP、online、SGI 计数 | 第二个核是否完成自身初始化 |
基础版 64 字节 frame 可用:
1 | |
精简版对应为 x/8wx frame,还要检查 Scheduler_Task 的 r4-r11。读错帧长度会把旁边的普通栈数据误认成寄存器。
如果停在汇编 BL 前,参数在 r0;进入 C 函数序言之后,r0 可能已被编译器复用,优先查看调试符号中的 frame 参数。调试寄存器必须和指令位置一起解释。
IAR 是调试时最容易误碰的寄存器
对 0xF8F0010c 做一次内存读取,可能就是一次 GIC acknowledge。为查看中断号而让 GDB 定时刷新这个地址,会改变程序本来要观察的状态。
优先查看软件已经读出的 iar、last_irq_id,以及 GIC 中适合诊断的其他寄存器。设备 FIFO、read-to-clear 状态也有类似问题;“只是看一眼”不一定是无副作用操作。
ELF 与反汇编检查能提前发现什么
1 | |
LOAD 段检查地址与权限,attributes 检查 CPU/ABI 相关标记,nm 看栈和页表的实际地址,objdump 看真正执行的指令。
特别要核对:
- 向量是否存在、是否 32 字节对齐;
- 一级表是否 16 KiB 对齐,coarse 表是否 1 KiB 对齐;
- 两核各模式栈是否不同;
- 汇编中 SRS/RFE 与预期分支是否真实生成;
- C 结构体偏移与指令使用偏移是否一致;
- 代码增长是否超出当前细粒度可执行映射窗口。
这些检查发生在运行之前,往往比启动崩溃后才查 C 代码更省时间。
几类值得长期保留的 Bug 经验
普通 ABI 规则被错误套到任意中断点
公共接口要求 SP 8 字节对齐,不代表函数内部每一条指令处都必须如此。IRQ 打断临时 4 字节对齐位置并不自动构成错误;应检查真正调用 handler 时的 SP。
现场正确,但存放位置会被覆盖
A 的 frame 留在共享中断栈,切到 B 后又从同一栈顶保存 B。第一次切换看似成功,恢复 A 时才失败。根因是所有权,不是单条 LDM 编码。
清设备状态和结束 GIC 服务只做了一半
只清源可能使 GIC 活动状态无法正常结束;只 EOI 可能让仍然有效的设备电平反复投递。记录 IAR→handler→W1C→EOIR 的顺序,比反复开关 CPSR.I 更有用。
把历史 tick 当作实时运行指标
基础版结束抢占测试时关闭 Private Timer,随后运行协同任务。旧 tick 固定是设计结果。Global Timer、任务 count 和 UI heartbeat 才是对应阶段的动态观察量;它们也各有单位,不能把 raw counter 当毫秒。
固定采样相位把任务 B 隐藏了
A/B 周期为 2,若每隔偶数次切换采一次,很可能总采到 A。界面看起来不切换,但任务计数都在增长。用独立时基或改变采样策略,再与纯文本日志交叉验证。
非重入串口输出被抢占
精简版允许 Timer 在 uart_puts 之间或内部打断任务。不同任务的字符可能交错,这不直接证明寄存器保存出错。若要得到整行原子输出,需要日志缓冲或合适的同步;不能为了显示整齐而无条件长时间关闭 IRQ。
功能、证据和边界放到一起
| 功能 | 当前实现/证据 | 仍需补齐 |
|---|---|---|
| 启动 | ELF、模式栈、BSS、UART、C 入口 | 真实板级时钟/DDR/Bootloader 交接 |
| 普通任务 | 独立栈、ABI context、A/B 恢复 | 生命周期、阻塞、优先级与通用就绪队列 |
| 异常 | 向量、frame、独立中断栈、RFE | 嵌套、用户异常、完整故障恢复 |
| 抢占 | Timer 请求、IRQ tail 选任务 | 更广的寄存器压力测试与可测延迟 |
| 精简现场 | 32 字节 frame、按需 r4-r11 | 同条件性能基准与通用初始化 |
| MMU | identity map、AP/XN 设计、PAR | 权限故障注入、动态映射、进程空间 |
| Cache | L1/L2/SCU 控制路径 | 真板一致性、DMA、相关勘误处理 |
| 双核 | CPU1 online、SGI 双向返回 | 完整 SMP 调度、迁移、并发共享结构 |
| 扩展状态 | 当前整数 soft-float 范围 | VFP/NEON、TLS、原子操作完整合同 |
这张表可以直接作为下一阶段的工程检查索引。已经完成的部分保持可运行,新增一个能力就补一条针对它的证据。
回顾时沿三个问题走
第一,当前执行流在哪里:哪个 CPU、哪种模式、哪块栈、哪份地址空间。
第二,要暂停它,需要保护什么:普通调用还是异步打断,哪些寄存器由 ABI 保护,哪些必须由入口保存,现场归谁所有。
第三,让它恢复,需要满足什么:寄存器布局、返回 PC/CPSR、GIC 服务结束、内存可见性、其他核是否仍可能修改相关状态。
沿着这三个问题,架构模式、AAPCS、上下文、GIC、MMU、Cache 和 SMP 会回到同一条可解释的执行链。遇到新的平台时,可以替换具体寄存器与启动协议,但保留这套分析顺序。
返回:系列目录与官方资料。
重点回看
可靠的移植记录必须同时给出功能、证据和边界。排错从 ELF 与反汇编开始,再沿启动、异常、GIC、设备和调度逐层缩小。不要读取有副作用的寄存器来做随手打印,也不要用单次输出代替可重复验证。当前成果是可运行的教学闭环,不是完整产品内核。
本章总结
可靠的移植记录必须同时给出功能、证据和边界。排错从 ELF 与反汇编开始,再沿启动、异常、GIC、设备和调度逐层缩小。不要读取有副作用的寄存器来做随手打印,也不要用单次输出代替可重复验证。当前成果是可运行的教学闭环,不是完整产品内核。
系列至此形成从架构规则到可运行系统、再到证据化调试的完整闭环。后续扩展可沿驱动、同步原语、完整调度器和真板启动合同继续推进。
ARMv7-A小系统实践(十二):用QEMU和GDB把整条实现链验清楚
https://goko-son626.github.io/post/armv7a-11-debug-review.html

