📑 本页目录
计算机系统 · 第七章配套讲义
把指令写成
能说清楚的程序第七章 · ARM程序设计
从数据放在哪里,到循环何时结束;从一层BL,到C与汇编互相调用。把每个寄存器、每个分支、每一格栈空间的责任讲清楚,再独立写出习题程序。
覆盖原讲义7.1—7.4全部小节、例7.1—7.51及第48页全部11道习题。页码对应独立第七章PDF:第1页目录,正文第2—47页。沿用项目内既有教材整理版样式。
开发链 → 伪指令与数据 → 顺序、分支、循环 → 子程序与ABI → C混合编程与CMSIS
从“程序怎样合作”开始 →先分清要求,再开始学
会独立写基础程序;工具与冷门语法知道何时查
本章没有跳过开发环境、伪指令目录、历史工具和特殊寄存器。每节标出掌握要求,再说明背景为什么要讲。重点按原章习题安排,实际考试范围结合老师要求。
| 内容 | 要学成什么样 | 对应讲解 |
|---|---|---|
| 开发链、交叉编译、IDE、探针 | 理解职责,会判断错误阶段;旧产品名认识即可 | 01—03 |
| 伪指令、数据、段、符号、表达式 | 常用的会写,其余会读会查;分清汇编期与运行期 | 04—15 |
| 启动、顺序、分支、循环、数组、排序 | 会分配寄存器、写代码、检查边界并手算 | 16—22 |
| 子程序、嵌套、幂与阶乘 | 保护LR和保存寄存器,不覆盖输出,0/1输入正确 | 23—25 |
| ABI、栈参数、C与汇编互调 | 简单整数/指针接口能独立写;复杂类型知道查ABI | 26—32 |
| CMSIS、特殊指令/寄存器 | 说清用途、工具依赖、权限与恢复;冷门名称认识即可 | 33—35 |
先看默认SVG,再换参数检查预测;接着做十二道短题,再完成全部原题。完整程序题由共同启动文件 + 各题全部应用文件组成,源码包也嵌在本HTML里,下载不依赖网络。
所有正文、MathML公式、脚本、默认打印SVG与源码均在本文件内;打印自动显示全部答案与默认图例,关闭脚本仍能读静态图。图是教学模型,不操作真实处理器。配套ArmASM工程有明确启动前提;核心算法和Thumb指令流经过核对,未在此环境实际运行MDK或连接实板。
第七章 · 第01节
这一章到底要把你教会什么
7.1、7.3、7.4;第 2、24—48 页
掌握要求:能把题目翻译为变量分工、控制流、访存与函数接口,再独立写出程序。
背景为什么要讲:开发工具也讲,因为一段正确的计算代码还需要被汇编、链接、放入正确地址,才能运行。
上一章解决的是“一条指令干什么”,这一章解决的是“几十条指令怎样合作完成一件事”。先不要同时背几十个伪指令。你真正要抓牢的是下面四件事。
| 问题 | 写程序时对应的动作 | 原章习题 |
|---|---|---|
| 数据在哪里? | 常量、数组、寄存器、结果地址分别安排 | 3—6、8—10 |
| 什么时候做、做多少次? | 比较、分支、循环、边界 | 3—6、8 |
| 函数怎样进出? | 参数、返回值、LR、寄存器保护、SP | 7—10 |
| 源程序怎样成为可执行程序? | 伪指令、段、符号、链接、启动 | 所有“完整程序”题 |
最重要的学习成果:能在纸上写出正确程序,并解释每个寄存器为何存在。 不要求第一次就记住 IDE 的每个按钮、旧工具所有版本号、全部冷门字符串运算符;这些内容本章都介绍,并标清查读边界。
一段程序有三种不同的“状态”:源文件里的文字;汇编、链接之后的代码与数据;运行时寄存器与内存里的值。比如 N EQU 10 只建立汇编期名称,MOVS R1,#10 才在运行时修改 R1。把这两个层次混在一起,后面的宏、循环、变量就会越看越乱。
本章以 Cortex-M3/M4 的 Thumb/Thumb-2 为运行范围;书中的 AREA/DCB/EXPORT 等属于 ArmASM 语法。为和原书对得上,主要程序沿用这一语法。GCC、armclang 集成汇编器的 .section/.byte/.global 是另一套写法,不能直接混贴;编译器差异在第 31 节集中说明。
重点依据是原章 11 道题及后续学习依赖,不是对你实际考试范围的预测。 背景会讲到,但不会把“认识工具名称”和“能独立写循环”安排成相同的背诵负担。
第七章 · 第02节
编辑、编译、汇编、链接、下载、调试各管哪一步
7.1.1—7.1.2;第 2—8 页
掌握要求:能解释交叉编译;能按报错阶段判断该找语法、符号、地址还是运行逻辑。
背景为什么要讲:工具名称经常换,开发链的职责相对稳定;理解职责比背界面截图更有用。
从“我写了一句 C”到“板子真的执行”,大致经过下面这些步骤。
| 环节 | 输入 → 输出 | 常见问题 |
|---|---|---|
| 编辑器 / 工程管理 | 人的想法 → .c/.cpp/.s/.h |
文件没加入工程、选错目标 |
| C/C++ 编译器 | 高级语言 → 目标代码;内部常经过汇编 | 类型、声明、编译选项不一致 |
| 汇编器 | 指令与伪指令 → .o/.obj、符号与重定位信息 |
指令不支持、立即数非法、伪指令方言不对 |
| 链接器 / 定位器 | 多个目标文件、库与地址布局 → ELF/AXF 映像 | 未定义符号、重复定义、段放不下 |
| 格式转换与下载 | 映像 → 可烧写的 HEX/BIN 等 | 地址丢失或下载算法、区域选择错误 |
| 调试器 | 可执行映像、符号、目标状态 → 可观察的执行过程 | 算法错误、越界、现场被破坏 |
ELF/AXF 往往带段、地址、符号与调试信息。HEX 记录地址和数据,裸 BIN 通常不自带完整的地址语义,下载时要指定装载位置。“编译成功”不等于“程序已被正确烧写并正确执行”。
交叉编译是:编译器运行在一种主机平台,却为另一种目标平台生成代码。Windows/x86 电脑生成 Cortex-M4 的 Thumb 代码就是一个例子;主机系统不是判断交叉编译的唯一条件,还要看目标体系结构和运行环境。
RTOS、函数库、评估板和调试探针不都属于“编译器”。RTOS 提供调度等运行服务;库提供可链接的功能;板子提供目标硬件;探针帮助下载和观察目标。IDE 把很多工具组合起来,但不改变各自的职责。
第七章 · 第03节
旧工具一口气认识完,MDK 学会怎么用
7.1.1 全部工具介绍、表 7.1;7.1.2;第 2—8 页
掌握要求:认识原书所有工具;会按目标芯片、工具链、存储区、启动文件和调试连接建立工程。
背景为什么要讲:版本与产品定位是历史背景,不要求背旧版本号;认识它们是为了看懂原书和旧工程。
书中的“最新”“支持所有”“最强”等表述有写作年代,不能当作今天的软件选型结论。这里保留它们在原章中的位置与作用。
| 名称 | 原章里的位置与要点 | 本阶段要记什么 |
|---|---|---|
| SDT | 早期 Arm 软件开发工具,后被 ADS 接替 | 历史名称,认识即可 |
| ADS | 集成编译、调试、模拟与库的旧工具套件 | 旧工程可能出现,认识即可 |
| RVDS | Eclipse 工程环境、RVCT 编译、RVD 调试、RVISS 指令模拟 | 四类职责能对应,不背缩写清单 |
| DS-5 | 原书用于较复杂 SoC、Linux/Android、跟踪与性能分析的套件 | 认识 Eclipse、Streamline、RTSM/FVP、DSTREAM 各自作用 |
| MDK / Keil MDK | 原章主要面向 MCU;μVision 集成工程、编译、调试、下载与模拟 | 本章 ArmASM 例子来自这一工具背景 |
| Arm 目标 GCC | 交叉工具链,不限于某一个主机系统 | 区分 Linux 用户程序目标与裸机 MCU 目标 |
arm-linux-gnueabi... 一类工具链面对 Linux 环境;arm-none-eabi... 一类常用于裸机/嵌入式环境。它们的运行库、启动和链接要求不同。不能因为名字里都有 Arm,就把 Linux 程序直接烧到 Cortex-M 上。
原表 7.1 还比较了编译器、IDE、内核支持、RTX/RTOS、中间件、调试器、模拟器、调试硬件、多核、GDB Server、逻辑分析仪等。这些是功能维度,具体支持要查所用版本、许可证、设备包和探针。 原表中“不支持多核”等结论属于当年的产品组合,不作为永久结论背诵。
一个工程至少核对六项
- 选对芯片/内核,例如本章原书使用 STM32F407ZG,不能把 M0 当 M4。
- 选对编译器与汇编语法;
AREA文件交给 ArmASM,不能误交给 GNU 方言汇编器。 - 配好 Flash/RAM 起始地址与容量;本章教学布局使用 Flash
0x08000000、RAM0x20000000。 - 加入启动文件和所有源文件;不要同时加入两个启动文件、两个同名入口。
- 设好链接布局和入口;对 C 工程还要有相应运行库初始化。
- 选择模拟器或目标探针,核对芯片下载算法,构建后再进入调试。
μVision 的工程窗口管文件与分组;代码窗口编辑;输出窗口看构建错误。进入调试后,寄存器窗口看 R0/SP/LR/PC,内存窗口看数组,反汇编窗口核对实际指令,观察/调用栈窗口看变量和调用关系。优化后“一行 C 对一条指令”通常不成立;单步汇编更适合核对本章算例。
“仿真器”这个词要分开理解
软件模拟器在主机上模拟目标;JTAG/SWD 调试探针通常连接并控制实际芯片;早期在线仿真设备又有别的硬件形式。原书列到 ULINK 系列、J-Link、DSTREAM、RVI/RVT 等名称,记住它们属于调试连接/跟踪工具即可。
探针并不是一律“替代 MCU”,也不保证完全不占片上资源。断点比较器、跟踪资源、调试引脚是有限的;某些内存观察可能影响实时性。SWD 与 JTAG 是不同调试接口,支持情况看目标和设备。品牌、版本号、营销评价不属于本章做题重点。
第七章 · 第04节
机器指令、汇编伪指令、伪操作不要混成一类
7.2 总述;7.3.1;第 8、24—25 页
掌握要求:能判断一行是在生成指令、定义数据,还是控制汇编;会按 ArmASM 四栏格式写语句。
背景为什么要讲:同叫“伪指令”的东西有不同用法,分清层次可以消除后面大半的困惑。
ADD 有架构定义的机器编码,运行时由 CPU 执行。AREA、DCD、EQU 等由汇编器处理:有的建立段,有的生成数据,有的只建立符号。它们本身不作为一条 CPU 指令执行。
另外,上一章的 LDR R0,=0x12345678 常称伪指令:汇编器可能用 MOV/MVN 等指令,也可能生成从文字池加载的 LDR。它和“完全不生成执行指令的 EQU”又不一样。做题时把“这行最终产生了什么”说清就行。
ArmASM 采用下面的传统列式写法:
; 标号顶格;操作助记符前留空白;分号以后是注释
Start MOVS R0, #0 ; R0 清零
Loop ADDS R0, R0, #1
CMP R0, #10
BNE Loop
四栏是 标号 操作助记符 操作数 ;注释。助记符与操作数之间是空白,操作数之间是英文逗号。标号区分大小写,Result 与 RESULT 不能随意混写;使用统一的大写助记符最省心。原书“标号不能加冒号、必须顶格”的要求针对这类 ArmASM 源文件格式,不是所有汇编器的统一规定。GNU 写法常用 Loop:,普通指令也不要求同样的首列规则。
把代码里的中文逗号、弯引号、全角等号替换为合法源代码字符。 书里的排版文字和能直接编译的源文件不是一回事。
第七章 · 第05节
EQU、GBL/LCL、SET、RN、RLIST:名字究竟代表什么
7.2.1、7.2.4 的 EQU/RN;7.2.5;例 7.1—7.4、7.22、7.28—7.31;第 8—10、17—21 页
掌握要求:能区分汇编期变量、常量、寄存器别名和真正的 RAM 变量;会读每一种定义。
背景为什么要讲:GBL/LCL 和字符串代换主要服务宏与可配置源文件,基础习题以会读为主。
| 写法 | 含义 | 会不会因此占一格 RAM? |
|---|---|---|
N EQU 10 |
给固定数值/地址表达式起名 | 不会 |
GBLA Count、Count SETA 3 |
汇编期数字变量,声明初值 0,再赋值 | 不会 |
GBLL Flag、Flag SETL {TRUE} |
汇编期逻辑变量,声明初值假 | 不会 |
GBLS Name、Name SETS "abc" |
汇编期字符串变量,声明初值空串 | 不会 |
LCLA/LCLL/LCLS |
相应的局部汇编期变量,常用于宏的局部环境 | 不会 |
Temp RN 0 |
Temp 是 R0 的别名 | 不会,也没有新增寄存器 |
Saved RLIST {R4-R7,LR} |
给一个寄存器列表起名 | 不会 |
Value DCD 10 |
定义一个有初始值的 32 位存储对象 | 会生成数据/存储安排 |
GBL 中的“全局”指汇编器处理的命名环境,不等于 C 语言的可链接全局变量。如果另一个目标文件要引用一个地址符号,要用 EXPORT/IMPORT 和链接;不是写一个 GBLA 就行。
N EQU 10
GBLA BuildCount
BuildCount SETA 3
GBLL UseFast
UseFast SETL {TRUE}
GBLS Greeting
Greeting SETS "Hello"
Temp RN 0
Saved RLIST {R4-R7,LR}
SETA/SETL/SETS 必须与声明类型一致;可以在汇编过程中更新。EQU 符号不能像 SETA 变量那样反复改值。同名普通符号不能重复定义。原书例 7.2 的 Test3/Test4/Test5 名称错配要改成声明哪个就赋给哪个。
$ 做汇编时代换。例如宏中 $dst 替换为实参寄存器名;字符串变量 S1="Test!" 时,字符串中的 $S1 可展开为该文本。数字代换按该汇编器规则转成十六进制文字,逻辑变量代换为真/假表示;它不是运行时去 RAM 读值。
RLIST 不改变存储顺序。无论列表文字怎样排列,多寄存器传输仍按架构的寄存器编号映射地址。RN 也不改变寄存器保护义务:把 R4 改名为 Temp 后,仍要按调用约定保存 R4。
原书混入的“EQU 能代表任意指令串,等号可给同名数值重新赋值”等说法不能跨汇编器套用。本章 ArmASM 中:常量用 EQU,汇编期可变值用 SETA/SETL/SETS;= 在相应数据定义语境可作为 DCB 别名,LDR Rd,=expr 又是另一种语法。 不把其他汇编器的规则混进来。
第七章 · 第06节
DCB、DCW、DCD:定义数组时先问每项多宽
7.2.2;例 7.5—7.7;第 10—11 页
掌握要求:会算字节数、数组元素地址、小端字节布局;能区分地址和该地址里的值。
背景为什么要讲:数据定义直接决定访存步长,是数组题最常见的失分来源。
| 定义 | 每项大小 | 常用读取 | i 项地址(i 从 0 起) |
|---|---|---|---|
A DCB 1,2,3 |
1 字节 | LDRB / LDRSB | A+i |
H DCW 1,2,3 |
2 字节 | LDRH / LDRSH | H+2i |
W DCD 1,2,3 |
4 字节 | LDR | W+4i |
DCB 可定义字节或字符串;不会自动给字符串补 \0。C 风格字符串必须明确写 Text DCB "ARM",0。数值 0 和字符 '0' 的编码 0x30 不是同一个字节。
AREA ConstData, DATA, READONLY, ALIGN=2
Bytes DCB 0x11,0x22,0x33,0x44
Words DCD 0x11223344,0x55667788
Text DCB "ARM",0
小端存储时,DCD 0x11223344 的低地址到高地址依次为 44 33 22 11。反过来,对 11 22 33 44 四个连续字节执行 LDR,得到 0x44332211。定义成 DCB,不妨碍用 LDR 一次读取四字节;但你必须检查对齐、可访问范围和题目想表达的类型。
DCW/DCD 会按该指令规则对首项做半字/字对齐,可能插入填充;DCWU/DCDU 不主动保证这种对齐。U 不表示“无符号”,表示不作相应对齐要求。不对齐的数据定义不等于任何指令都能合法访问它。 多寄存器、双字等访问还有各自限制。
LDR R0,=Words 通常是取地址;LDR R1,[R0] 才是读内容。习题需要“把结果放入 R0”就让 R0 保存数值;如果要“存入 Result 单元”,还要取 Result 地址并 STR。不要把结果地址误当作结果。
第七章 · 第07节
DCQ、DCFS、DCFD、SPACE:数据格式与初始化分开看
7.2.2;例 7.8—7.11;第 11—12 页
掌握要求:知道所有数据定义的宽度和含义;明确 SPACE、NOINIT 与 C 启动初始化的关系。
背景为什么要讲:浮点定义并不等于处理器具备相应浮点运算能力;这些是读程序和估算布局所需的背景。
| 伪指令 | 每个元素 | 存的是什么 | ArmASM 这一类定义的对齐 |
|---|---|---|---|
| DCFS / DCFSU | 4 字节 | 单精度浮点表示 | 非 U 版为字对齐 |
| DCFD / DCFDU | 8 字节 | 双精度浮点表示 | 非 U 版为字对齐 |
| DCQ / DCQU | 8 字节 | 64 位整数位串 | 非 U 版为字对齐 |
| SPACE n | n 字节 | 预留空间;是否有映像初值受段属性等约束 | 自身不替代所需 ALIGN |
每项 8 字节,不代表这个伪指令自动做到 8 字节对齐。 ArmASM 的 DCQ/DCFD 常规形式按 4 字节边界安排;若接口或访问需要 8 字节对齐,再明确 ALIGN 8 或相应的段对齐。
AREA Tables, DATA, READONLY, ALIGN=3
ALIGN 8
Big DCQ 100,101,102
F32 DCFS 0.5,-1.0
F64 DCFD 0.5,-1.0
十进制 100 就是 100,写成十六进制是 0x64;原书 DCQ 例子把十进制说明和十六进制文字混在一起,不能据此改变数值。DCD 1 是整数位串 0x00000001;DCFS 1.0 是浮点 1.0 的表示 0x3F800000;LDR 只搬位串,不会自动把一种转换为另一种。
M4 的可选 FPU 主要支持单精度算术;你依然可以在内存里保存双精度数据,必要时由软件运算。能定义 DCFD 不证明有硬件双精度加法。
普通初始化数据区域中,SPACE 通常形成零填充;在 NOINIT 等区域,它用来预留不带初始化要求的空间。AREA ...,DATA,READWRITE,NOINIT 下的栈空间不应该假定上电全为零。链接映像给 RAM 数据规定了初始内容,也需要启动代码负责拷贝/清零,不能认为写了 DCD 或 SPACE,芯片上电就自动执行这些“初始化语句”。
本章习题工程把输入常量放在只读数据段,结果区先写后读,避免依赖尚未讲清的 C 运行库初始化。通用 C 工程的 .data/.bss 初始化交给匹配的启动文件和运行库;第 16 节说明。
第七章 · 第08节
MAP/FIELD:只画户型,不替你分配房间
7.2.2;例 7.12—7.13;第 12 页
掌握要求:会累计字段偏移;能明确说明 MAP/FIELD 不分配实际存储。
背景为什么要讲:这是结构化地址描述的方法,基础习题不要求熟练写复杂映射,但读外设与结构定义会遇到。
MAP 0x100
A FIELD 16
B FIELD 32
S FIELD 256
起点是 0x100,A 占用的描述长度为16,所以 B 的符号值为 0x110;B 再描述32字节,所以 S 为0x130;结束位置为0x230。A 范围是0x100—0x10F,不是连0x110也算进去。
这些符号只是结构的偏移/地址约定。没有因此生成 304 字节 RAM 或 Flash 数据。 真要一块存储空间,另用 SPACE 或数据定义,或使用真实的内存映射硬件区域。
带基址的 MAP 0x100,R0 描述的是“R0+0x100 起的一组寄存器相对地址”;不把当前运行时的 R0 数值在汇编时算出来。若 A 从该位置开始,:BASE:A 可描述其基址寄存器编号,:INDEX:A 可取其中的偏移部分。可以理解成“基址寄存器 + 字节偏移”的两部分说明。
FIELD 通常按指定长度推进,不会替你模拟 C 编译器所有结构体填充规则。和 C 的结构共享时,要核对字段偏移、对齐、总长、打包选项,而不是只看字段名字一样。
第七章 · 第09节
汇编期 IF/WHILE 与运行时 BEQ/BNE 是两回事
7.2.3;例 7.14—7.15;第 13—14 页
掌握要求:能写出条件汇编后实际保留下来的代码;理解汇编期循环必须推进自己的计数变量。
背景为什么要讲:用于编译配置与重复生成,通常不是统计数组元素时使用的运行时循环。
GBLL Fast
Fast SETL {TRUE}
IF Fast
MOVS R0,#1
ELSE
MOVS R0,#2
ENDIF
汇编器看到 Fast 为真,就只保留对应分支产生的代码;程序运行时不再做一次“Fast 是否为真”的判断。反过来,CMP R1,#0 / BEQ ZeroPath 是运行时比较寄存器,代码里会同时有两条可能路径。
GBLA K
K SETA 0
WHILE K < 3
DCB K
K SETA K+1
WEND
生成三个字节 00 01 02。CPU 不执行 WHILE、SETA 或 WEND;汇编器把数据先生成好了。原书 Counter<10 的 WHILE 例子省略了 Counter 的更新,照抄可能让汇编过程无法终止。生成代码/数据的循环也需要退出条件。
IF/ELSE/ENDIF、WHILE/WEND 可以嵌套,但层次要配对。真值可以来自逻辑变量、表达式或 :DEF:Feature。不要用运行时寄存器的内容来控制普通汇编期 IF;汇编器并不知道程序未来执行时 R0 是多少。
题目统计 10 个数据时,应该写 LDR/TST/CMP 与 Bxx 的运行时循环。 用汇编期 WHILE 重复写十段代码不是本题“运行时遍历”的通常学习目的。
第七章 · 第10节
宏展开、MEXIT 与子程序:文字复用和运行复用
7.2.3;例 7.16;第 14—15 页
掌握要求:看懂宏参数、标号代换、展开结果;解释宏与 BL 子程序在代码大小和现场管理上的差别。
背景为什么要讲:宏能减少源文件重复,但不自动减少机器代码,也不自动保证更快。
MACRO
$label UMax $a,$b,$out
CMP $a,$b
BHI $label.take_a
MOV $out,$b
B $label.done
$label.take_a
MOV $out,$a
$label.done
MEND
One UMax R0,R1,R2
Two UMax R4,R5,R6
汇编器将 One、Two 各自展开为一段代码;$label 让内部标号分别成为 One.take_a 与 Two.take_a 等,避免撞名。参数是文本实参,例如 R0,不是 C 风格自动建立的参数栈。这里按无符号比较取大值;若改成有符号需要改条件码。
MACRO/MEND 包围定义;宏原型可以有可选标号、参数和默认值。MEXIT 提前结束当前宏展开,不是 CPU 从函数返回。它常与汇编期 IF 配合,按参数/配置选择是否继续生成。
| 宏 | 子程序 |
|---|---|
| 每个使用点展开一份代码 | 通常只有一份主体,多处 BL 过去 |
| 没有 BL/返回这套固定调用开销 | 需要返回链接、约定和可能的保存恢复 |
| 多次使用可能变大 | 适合复用较大功能 |
| 仍可能破坏寄存器与标志 | 按接口约定管理现场 |
不能简单说“宏一定快”或“子程序一定提高运行速度”。要看展开大小、分支、缓存/流水线与编译优化。做题先保证逻辑正确、标签唯一、所用寄存器有约定;性能结论属于进一步测量。
第七章 · 第11节
AREA 是段的说明,链接器才决定放到哪里
7.2.4 的 AREA;7.3.2;例 7.17;第 16、25—26 页
掌握要求:读懂 CODE/DATA、只读/读写、NOINIT 和段对齐;不把“数据段”等同于“RAM 已有初值”。
背景为什么要讲:完整程序必须既有代码和数据的身份,又有实际内存位置。
AREA AppCode,CODE,READONLY,ALIGN=2
AREA Inputs,DATA,READONLY,ALIGN=2
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
第一行声明代码;第二行是只读数据,适合放 Flash 常量;第三行是可写但不承诺启动初始化的结果区,适合放 RAM。DATA 不意味着一定可写,更不意味着自动放到 0x20000000。 具体执行/装载地址由链接布局决定。
| AREA 属性 | 应怎样理解 |
|---|---|
| CODE / DATA | 段内容类别 |
| READONLY / READWRITE | 说明段读写属性;最终保护还取决于实际存储器和 MPU 等 |
| ALIGN=n | 段起点要求按 字节对齐 |
| NOINIT | 不要求普通启动初始化;不能依赖随机上电内容 |
| COMMON | 按链接规则合并的同名公共数据区域,不是普通代码共享开关 |
多个源文件都可以有段;段名与链接后地址不是一一固定关系。以数字开头或含特殊字符的名称可能要用 |...| 形式。一般保持清晰字母名称即可。
代码段里也能夹常量数据,例如文字池、查找表;但 CPU 不能顺序跌落去执行这些数据。可写数组应放真实可写区域;不要把 Flash 中的 DCD 数组当 RAM 去交换排序。
原书第 25 页 Num=0x11、Nums=0x22 的更新例子,最终是 Num=0xAB、Nums=0xCD。这个结论依赖初值已在 RAM 就位。本章第 17 节改成显式建立初值,避免把调试器帮忙装载 RAM 的效果误认为上电必然发生。
第七章 · 第12节
ALIGN 4 与 ALIGN=2,看着相似,参数意义不同
7.2.4 的 ALIGN、AREA ALIGN;例 7.18;第 16 页
掌握要求:会计算下一个满足边界的位置和填充字节;区分段对齐指数与 ALIGN 的字节边界。
背景为什么要讲:对齐会改变标号地址,直接影响数组、结构、文字池与栈。
在 ArmASM 中,AREA ...,ALIGN=3 表示段起点按 8 字节对齐;独立的 ALIGN 8 表示当前位置按 8 字节边界对齐。 一个参数是指数,一个是边界字节数。
设当前位置 a,对齐边界 b 是 2 的幂、不使用额外偏移:
若当前为0x1003,ALIGN 4 后变为0x1004,补1字节;若当前已为0x1004,就不需要再跳到0x1008。
ALIGN b,o 要找不小于当前位置、形如 o+k*b 的位置,k为整数,不是固定只算 2b+o。默认 ALIGN 常用于4字节边界。填充的内容与代码/数据、选项有关;基础地址题主要算填充长度,不机械认定任何情形都同一字节值。
原书例 7.18 的 ALIEN=3 是排版错误,应为 ALIGN=3;它约束的是段起点,不是承诺“以后每一条指令都相隔8字节”。Thumb 指令本来可为2或4字节,不能因一条段对齐属性而改变所有指令长度。
做数据布局题的固定顺序:先看当前偏移 → 按定义规则补对齐 → 给该标号地址 → 加上数据长度。 碰到 U 版数据定义,要重新判断是否自动补齐。
第七章 · 第13节
THUMB、CODE16/CODE32、ENTRY、END:谁也不会自动让 CPU 停机
7.2.4;例 7.19—7.21;第 16—17 页
掌握要求:知道这些是汇编/链接说明;Cortex-M 使用 Thumb;区分文件结束、入口元数据与硬件复位向量。
背景为什么要讲:书里混用了经典 A32 和 M 系列模型,先限定架构才能写出可执行程序。
ARM/CODE32 指示后续按 A32 汇编;传统 CODE16 对应旧 Thumb 语法环境。现代统一语法的 M3/M4 程序使用 THUMB,其中仍然可能生成16位或32位编码。不要把“Thumb”翻译为“所有指令都是16位”。
这些指示不会在运行时切换处理器状态。经典能执行 A32/T32 的内核可以借助 BX 等完成状态交换;Cortex-M3/M4 只有 Thumb 执行状态,不能按原书 CODE32/BX 的演示切去 A32。函数指针/复位入口的最低位需要正确表示 Thumb 入口,实际指令地址仍是对齐地址。
ENTRY 给工具链入口信息;最终 ELF 入口可以由链接选项等指定。普通被调用的汇编函数不需要自己声明为主入口。ENTRY 不是 Cortex-M 的向量表,也不能替代复位时的初始 MSP 和 Reset_Handler。
END 告诉 ArmASM:这个源文件到此结束。它不生成一条“CPU 退出程序”的指令,也不是每个 AREA 后都写一次。多个段仍可以属于同一个源文件,通常在文件末尾写 END。函数由 BX LR 等返回;裸机程序需要自己安排持续运行或安全停留。
THUMB
AREA AppCode,CODE,READONLY
Stop
B Stop ; CPU 在这里继续执行分支
END ; 汇编器到这里结束读源文件
不能让执行流越过最后一条指令,指望 END 拦住 CPU。 后面可能是文字池、数据、空白或别的函数。
第七章 · 第14节
EXPORT/IMPORT、包含文件、弱符号和局部标号
7.2.4;例 7.23—7.28;第 17—19 页
掌握要求:会把两个源文件接起来,分清文本包含、二进制嵌入与真正的目标文件链接。
背景为什么要讲:弱符号与 ROUT 主要用于启动文件、库和宏;基础题认识用途与边界即可。
| 需求 | ArmASM 做法 | 要点 |
|---|---|---|
| 本文件定义函数,供外部调用 | EXPORT func,GLOBAL 可作相应别名 |
名称、大小写一致 |
| 函数/变量由别的文件定义 | IMPORT func 或 EXTERN |
由链接器找到定义 |
| 把宏/常量定义文本引进来 | GET file.inc,或 INCLUDE |
文本在此处继续被汇编 |
| 原样嵌入资源字节 | INCBIN image.bin |
不解析内部符号,不等于链接 .o |
| 限定数字局部标号搜索范围 | ROUT |
不是给普通 RAM 变量划定作用域 |
IMPORT 与 EXTERN 都用于外部符号引用;传统 ArmASM 中有“是否在未实际引用时也留下符号项”等细节,实际以版本说明为准。当前重点是:声明外部引用不等于提供实现,链接时仍需加入定义它的目标文件。
[WEAK] 用于弱定义/弱引用等链接语义:强实现可覆盖默认处理函数,某些未解析弱引用允许按工具链规则处理。不要把“没定义就自动当 NOP”当成所有弱调用的安全保证;实际链接、目标和调用形式会影响结果。关键函数应有确定实现。
原书说 INCBIN 可包含目标文件,容易误导:即使把一个 ELF .o 原样嵌进去,也只是嵌入那些文件字节,不会自动完成重定位、符号解析和库搜索。 真正的目标文件必须交给链接器。
ROUT 管的是数字局部标号的搜索范围,例如局部循环目标,配合该汇编器的前/后向引用语法使用。它不替代 C 的局部变量、栈帧或宏局部变量声明。基础程序使用清晰且不重名的普通标签已足够;读启动文件时知道 ROUT 的职责就行。
第七章 · 第15节
所有表达式和符号都过一遍:算出常量,不是执行算法
7.2.5—7.2.6;例 7.29—7.35;第 20—24 页
掌握要求:能求常见数字表达式;认识全部逻辑、字符串和寄存器相关运算符;区分编译期字符串长度与运行时 strlen。
背景为什么要讲:完整运算符目录不需要死背;保留是为了读宏和查阅旧源文件时不会漏掉概念。
表达式由常量、变量、运算符和括号构成,在汇编时求值。MOVS R3,#(3+4) 的执行效果是“装入7”,CPU 不再额外做一条加法。结果还必须满足该机器指令的立即数编码规则。
| 类别 | 原书涉及的运算符 | 例子/结果 |
|---|---|---|
| 算术 | + - * / :MOD: |
9:MOD:4 得1;整数除法不是浮点除法 |
| 位移/循环移位 | :SHL: :SHR: :ROL: :ROR: |
9:SHL:1 得18 |
| 按位逻辑 | :AND: :OR: :EOR: :NOT: |
9:EOR:1 得8 |
| 比较 | = > < >= <= /= <> |
不等比较产生逻辑真假 |
| 逻辑运算 | :LAND: :LOR: :LEOR: :LNOT: |
对真假值运算,不是对每个数据位运算 |
| 字符串 | :LEN: :CHR: :STR: :LEFT: :RIGHT: :CC: |
长度、字符、数值转串、左右子串、拼接 |
| 地址描述 | :BASE: :INDEX: |
从寄存器相对表达式取基址编号与偏移 |
| 符号/代码信息 | :DEF:、?label |
是否定义、标号所在定义行所生成的字节数 |
字符串例子::LEN:"ARM" 得3;:CHR:65 得字符 A;"Hello":LEFT:2 得 He;"Hello":RIGHT:2 得 lo;"A":CC:"RM" 得 ARM。:STR: 将数字按该汇编器格式变为十六进制字符串,将逻辑值变为对应的真/假文本。源书提到512字节字符串上限,作为旧工具的实现边界认识即可,不是 Cortex-M 的硬件字符串限制。
:LEN:"ARM" 在汇编期就知道答案;第10题的字符串指针指向运行时内存,要用 LDRB 循环数到0,不能用 :LEN: 代替。
符号必须在相关作用域内唯一,区分大小写,不与保留字/寄存器/助记符撞名。数字常量注意进制:10、0x10分别是十进制10、16;32位寄存器位串既可解释为无符号,也可解释为补码有符号,不能仅凭外观决定。
原书 LA MOV R2,#1 后 ?LA 得4,是特定编码条件下的结果;在 Thumb 中指令可能为2字节,不能总写4。并且立即数写法要使用相应 #。?label 可涉及数据定义行长度,也不能概括成“任何标签对应一个固定4字节机器指令”。
求值时先算括号,单目运算优先级高,同级双目通常从左到右;实际编程主动加括号。:NOT:3 这样的全宽位取反要注意工具表达式宽度,若目的是固定32位常量,可直接用 LDR R0,=0xFFFFFFFC 清楚表达,不混同运行时 MVN。
第七章 · 第16节
“完整程序”还差什么:复位、栈和初始数据
7.3.2;7.3.3;7.4.1;第 25—26、34、40—42 页
掌握要求:分清完整函数、完整汇编模块、完整可启动工程;会解释本章练习的启动文件、段和链接条件。
背景为什么要讲:原书有的例子依靠调试环境直接进入 ENTRY;补启动过程是为了不把“调试器能跑”误认为“上电一定能跑”。
本章的基础题先学算法,不让芯片时钟和外设初始化淹没重点。但凡称“完整程序”,至少要交代从哪里进入、SP怎样有效、输入在哪里、结果在哪里、结束后去哪里。
对 M3/M4,硬件复位读取向量表:第0个字是初始 MSP,第1个字是 Reset_Handler 的 Thumb 入口。CPU不是通过执行 ENTRY 开始。Reset_Handler随后转到应用;有普通 C 全局初值时,还要完成数据拷贝、零区初始化及相应运行库启动。
本章附带的最小练习工程
每题由 startup.s + 该题 app.s 组成;调用 C 的题再加入对应 .c。下列启动文件在各题共用,全文与源码包均提供。练习假定 STM32F407ZG 类布局:Flash从0x08000000开始,所用RAM从0x20000000开始;实际芯片须核对容量和启动映射。
PRESERVE8
THUMB
Stack_Size EQU 0x400
AREA STACK,NOINIT,READWRITE,ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
AREA RESET,DATA,READONLY,ALIGN=7
EXPORT __Vectors
__Vectors
DCD __initial_sp
DCD Reset_Handler
DCD Default_Handler ; NMI
DCD Default_Handler ; HardFault
DCD Default_Handler ; MemManage
DCD Default_Handler ; BusFault
DCD Default_Handler ; UsageFault
DCD 0,0,0,0
DCD Default_Handler ; SVC
DCD Default_Handler ; DebugMon
DCD 0
DCD Default_Handler ; PendSV
DCD Default_Handler ; SysTick
AREA StartupCode,CODE,READONLY
IMPORT exercise_main
EXPORT Reset_Handler
Reset_Handler PROC
CPSID I
BL exercise_main
Halt
B Halt
ENDP
Default_Handler PROC
B Default_Handler
ENDP
END
这个启动文件不调用 C 运行库:只读输入放 Flash,NOINIT 结果先写后读;C算例不读未初始化全局变量、不使用标准库。它有16个系统向量入口,练习期间屏蔽可配置中断,不包含具体芯片完整的外部中断向量和外设初始化。这是针对本章无外设练习的最小程序,不是任意产品工程的通用启动文件。 NMI等仍可能进入默认处理函数。
链接布局必须让 RESET 先放到 Flash 起点,把 STACK 和可写数据放进RAM。源码包的 layout.sct 给出了这两块区域及“RESET在最前”的规则。启动文件里的 STACK预留1024字节,顶端标签作为初始MSP,按8字节对齐;不是从随手写的地址借一块可能被其他变量占用的空间。
结果标号如 Result 在调试器内存窗口查地址;相应题若要求R0/R3,也保留该寄存器结果。应用返回后启动文件停在 Halt;不要通过重置或继续执行未知字节“查看答案”。
若使用厂商完整启动/C模板,应该保留其运行库初始化,让你的函数作为 main 或 main调用的函数接入;不能同时加入本章 startup.s 和厂商另一个启动文件。GNU/armclang工程还需换用对应汇编指示和链接脚本,不能只改扩展名。
第七章 · 第17节
顺序程序:先取地址,再读值,最后写结果
7.3.2;7.3.3 顺序结构;例 7.36;第 25—26 页
掌握要求:能追踪每一次 LDR/STR 的地址与值;会解释按字节定义的数据为何能读出一个字。
背景为什么要讲:顺序结构是分支循环内部的基本积木,必须算熟。
先修正原书Num/Nums例子的初始化前提。下面在 RAM 里显式写初值,再计算:
LDR R0,=Num
MOVS R1,#0x11
STR R1,[R0] ; 明确建立初值
ADDS R1,R1,#0x9A
STR R1,[R0] ; Num = 0xAB
LDR R0,=Nums
MOVS R2,#0x22
STR R2,[R0]
ADDS R2,R2,#0xAB
STR R2,[R0] ; Nums = 0xCD
Num/Nums必须在可写区域并有空间。上面是应用内部的算法片段,不是带向量表的独立工程。
原例7.36的字节数组低地址依次是:
ALIGN 4
Arr DCB 0x11,0x22,0x33,0x44
DCB 0x55,0x66,0x77,0x88
DCB 0,0,0,0
若 R0=Arr,则 LDR R2,[R0] 得0x44332211;R1=1 时,LDR R3,[R0,R1,LSL #2] 读取 Arr+4 处四字节,得0x88776655。不是读取“第1个字节”,而是从首字节的地址开始,读一个32位字。
按数组元素访问时,步长由元素宽度决定,且下标必须落在范围内。若数组共12字节,LDR从Arr+9读取4字节会越过数组结尾,即使那个地址仍在RAM中,也不再是这个数组的合法访问。
做顺序程序题可以画三列:指令、访存地址、执行后寄存器/内存。遇到 LDR =label 单独写“地址”,遇到 [Rn] 才填“内容”。这是最省脑子的检查法。
第七章 · 第18节
64 位加法:低字先算,存储顺序另行约定
例 7.37;第 27 页
掌握要求:会按 ADDS→ADC 连续传递进位;能分清数组的“高字在前”约定与通常的小端 uint64_t 布局。
背景为什么要讲:多字运算把上一章的 C 标志用到程序中,也是后面长整数算法的基础。
原书明确把数组首字解释为高32位,第二字为低32位:
Arr1 DCD 0x11223344,0xFFDDCCBB
Arr2 DCD 0x11223344,0xFFDDCCBB
这是程序自定的高字在前格式,不是通常小端 C uint64_t 的低字在低地址格式。每个DCD内部仍然按小端排列字节;“字内部字节顺序”和“程序指定两个字谁是高部”是两层问题。
LDR R0,=Arr1
LDR R1,[R0] ; 高字
LDR R2,[R0,#4] ; 低字
LDR R0,=Arr2
LDR R3,[R0]
LDR R4,[R0,#4]
ADDS R6,R2,R4 ; 低部,产生 C
ADC R5,R1,R3 ; 高部,加上传来的 C
LDR R0,=Result
STR R5,[R0] ; 结果仍按高字在前
STR R6,[R0,#4]
低字相加:0xFFDDCCBB+0xFFDDCCBB=0x1FFBB9976,留下0xFFBB9976、C=1。高字相加:0x11223344×2+1=0x22446689。结果为 0x22446689FFBB9976。
ADDS和ADC之间不能插入会覆盖C的指令,比如一次不相关CMP。装载/存储本身通常不更新算术标志,但写代码时仍要按实际指令看。
上面是主程序片段,使用R4—R6没有对外接口;若把它改成供C调用的函数,就需要按ABI保护所修改的callee-saved寄存器。若要把结果当作标准小端uint64_t,存储时通常低字先放低地址,再放高字,不能只改类型名不改布局。
第七章 · 第19节
分支:先决定比较类型,再画两条会合的路径
7.3.3 分支结构;例 7.38—7.43;第 27—30 页
掌握要求:会写 if/else、最大值与三路符号判断;明确执行比较时使用的是修改前的数据。
背景为什么要讲:原章混用A32条件执行和Thumb,统一为M3/M4可读的显式分支,必要时说明IT。
CMP R0,R1 相当于只更新标志的R0−R1。有符号“大于”用GT,无符号“大于”用HI;相等都用EQ。题目没有说明符号时先声明假设,不要让同一段程序前后换解释。
以原书 x=76,y=88; if(x>y) z=100; else z=50; 为例,若x/y是有符号int:
CMP R0,R1
BGT Greater
MOVS R2,#50
B Join
Greater
MOVS R2,#100
Join
R2统一装z。原书把else写到R1、注释却说R2,是寄存器笔误;源例用HI则是无符号条件,对76/88恰好结果相同,对负数就未必。
例7.38—7.40分别是无条件分支、比较后相等分支、直接目标分支。真实B仍有编码范围与目标对齐要求,不能认为任意绝对地址都能直接编码。例7.41可写:
CMP R5,#10
BEQ Eq10
MOV R0,R5
B Done10
Eq10
MOV R1,R5
Done10
这里不被赋值的那个寄存器保留旧值,不能擅自说另一边也清零。
例7.42取两个无符号数最大值,下面为应用内片段,Arr1/Arr2为已定义的32位输入,Result是可写结果单元:
LDR R2,=Arr1
LDR R0,[R2]
LDR R2,=Arr2
LDR R1,[R2]
CMP R0,R1
BHS SaveMax
MOV R0,R1
SaveMax
LDR R2,=Result
STR R0,[R2]
原书直接用标签访存的简写在PC相对范围/合法寻址方面有额外条件。这里显式取地址,更容易同时处理Flash输入和RAM结果。例7.43的有符号符号函数,输入X已在R2,输出Y到R0:
CMP R2,#0
BEQ IsZero
BGT IsPositive
MOVS R0,#0
MVN R0,R0 ; -1 = 0xFFFFFFFF
B SignDone
IsPositive
MOVS R0,#1
B SignDone
IsZero
MOVS R0,#0
SignDone
若将结果写回输入单元,相当于覆盖X;若要保留X,则另设Result。这里给的是算法片段,需接在明确的入口与结束路径中。
原书的MOVLT/MOVHI等条件数据处理,在M3/M4统一语法中要用相应IT块或由工具明确生成;本章使用显式Bxx,避免靠自动IT行为。BL负责建立返回链接;BX寄存器目标和A32/T32历史切换的边界见第13、23节,不把普通跳转和函数调用混用。
第七章 · 第20节
for、while、do-while:初始化、测试、更新一个都不能少
7.3.3 循环结构;第 30—31 页
掌握要求:能说明循环体执行多少次;会处理0次情况,识别更新或比较顺序导致的死循环。
背景为什么要讲:汇编没有替你打包好的for语法,循环的每一项责任都要显式写出。
原书 for(i=0;i<10;i++) x++; 的基本转换:
MOVS R0,#0 ; x
MOVS R2,#0 ; i
ForTest
CMP R2,#10
BHS ForEnd ; unsigned i>=10
ADDS R0,R0,#1
ADDS R2,R2,#1
B ForTest
ForEnd
循环体恰好执行10次。测试放入口允许0次:若初始i已经不小于10,直接退出。SUBS count,#1; BNE 是常见倒计数结构,但若count初值为0而直接进入循环,先减后会下溢为0xFFFFFFFF。
while(x<=y) x*=2,x=1、y=20,可用每次重做CMP的形式:
WhileTest
CMP R0,R1
BHI WhileEnd
LSLS R0,R0,#1
B WhileTest
WhileEnd
检查序列1→2→4→8→16→32,结果32。若范围允许32位溢出,翻倍可能绕回0而无法退出;题目必须限定数据范围或增加溢出处理。
原书 MOVLS ...; BLS ... 依靠同一次CMP标志;若中间实际生成的移位指令更新了标志,BLS看到的就可能不是原比较结果。上面把出口放在CMP之后,并在下一轮重新比较,更便于验证。
for与while先测试再执行,可能0次;do-while先执行再测试,至少1次。写循环之前,先用N=0、1、2在纸上跑一遍。 这是发现差一、漏更新和多读取一个元素的有效办法。
第七章 · 第21节
求和、统计偶数、统计正数:同一个遍历骨架
例 7.44;习题4—6;第 31、48 页
掌握要求:能写计数遍历;统计的是值满足条件的元素数量;有符号正数严格大于0。
背景为什么要讲:这三道题共享循环结构,学会骨架比背三份答案更稳。
遍历数组的角色可以固定成:R1指针、R2剩余元素数、R3当前值、R0计数结果。32位数组每轮指针前进4字节,剩余数减少1。
MOVS R0,#0
LDR R1,=Values
MOVS R2,#10
CountLoop
LDR R3,[R1],#4
TST R3,#1
BNE NotEven
ADDS R0,R0,#1
NotEven
SUBS R2,R2,#1
BNE CountLoop
偶数的最低位为0,包括0和负偶数。题目问“偶数的个数”,不是“偶数下标的元素个数”,不是“把偶数相加”,也不是“每隔一个取一个”。数组[-4,-3,0,1,2,3,4,5,6,7]有5个偶数。
若统计有符号正数,改成 CMP R3,#0; BLE NotPositive; ADDS R0,#1。0不算正数,0xFFFFFFFF代表−1,也不算正数;不能用无符号BHI代替BGT。上述数组有7个正数。
求1到100之和可以递增相加,也可倒计数相加。推荐先理解:每轮把当前k加进sum,再k减1;k从100到1,恰好100轮,结果5050。公式 用于核对结果,不替代题目要求的循环。
对一般N,先判断N是否为0;32位累加还要限制范围或处理溢出。本章100固定值不会溢出。第4、5、6题给出带数据、结果区、入口与共用启动文件的完整应用模块。
第七章 · 第22节
冒泡排序:每轮到底把谁送到右端
例 7.45;第 31—32 页
掌握要求:会写从大到小排序;知道外层n−1轮、内层逐轮缩短;相邻访问不越界。
背景为什么要讲:原例有方向、边界和注释冲突,必须用一个自洽版本重新建立直觉。
目标是把[12,7,19,8,24]排成 [24,19,12,8,7]。从左向右比较相邻项,若左<右就交换,让较小值向右移动。每轮结束,当前区间的最小值固定到右端。
| 状态 | 数组 |
|---|---|
| 初始 | 12, 7, 19, 8, 24 |
| 第1轮后 | 12, 19, 8, 24, 7 |
| 第2轮后 | 19, 12, 24, 8, 7 |
| 第3轮后 | 19, 24, 12, 8, 7 |
| 第4轮后 | 24, 19, 12, 8, 7 |
下面是明确按剩余比较次数控制的程序片段。SRC必须是已初始化的可写数组;本章最小启动工程若要运行它,应先把只读初值拷到RAM,再排序。
MOVS R4,#4 ; 本轮相邻比较次数 n-1
Outer
LDR R1,=SRC
MOV R5,R4
Inner
LDR R2,[R1]
LDR R3,[R1,#4]
CMP R2,R3
BHS NoSwap ; unsigned 左>=右,已是降序
STR R3,[R1]
STR R2,[R1,#4]
NoSwap
ADDS R1,R1,#4
SUBS R5,R5,#1
BNE Inner
SUBS R4,R4,#1
BNE Outer
第一轮4次,最后一次读下标3和4;不会读下标5。之后3、2、1次,总共 次比较。一般化到n=0或1时,必须在做n−1之前/进入循环之前处理,不然会下溢。
原书代码用STRHI在左>右时交换,倾向升序;却给了降序结果。又把SRC+LEN(尾后地址)当成最后元素地址,可能多读一个元素;“最大的数到数组尾”也与降序过程不符。记住比较方向、最后一对下标和每轮固定的元素,别背那份冲突代码。
第七章 · 第23节
子程序最小接口:参数进来,结果出去,链接保住
7.3.3 子程序;例 7.47;第 32—36 页
掌握要求:会定义标签/PROC、用BL调用、用BX LR返回;返回值优先按约定放R0。
背景为什么要讲:原例把最大值放R2是自定接口,可以用,但必须和C常见接口分清。
MOVS R0,#19
MOVS R1,#20
BL UMax32
; 此处 R0=20
UMax32 PROC
CMP R0,R1
BHS MaxDone
MOV R0,R1
MaxDone
BX LR
ENDP
这是无符号最大值函数:输入R0/R1,输出R0,可能修改条件标志。PROC/ENDP 为工具提供函数边界信息,本身不会替你保存寄存器或产生返回指令。若从其他文件调用,还需要EXPORT/IMPORT。
BL把调用后的返回位置编码到LR,并跳到目标;不是简单把某个未解释的“当前PC读值”原样保存。M系列的返回链接带Thumb标识。BX LR按返回链接返回;普通返回不应写成BLX LR,因为后者再次建立链接、修改LR,语义不是普通返回。
叶函数是不再调用别的函数的函数。如果它不需要借用LR作别的用途,通常可以直接BX LR,不必无条件先压栈。调用别人的非叶函数则要在下一次BL覆盖LR之前保存自己原来的返回链接。
原例7.47约定R2为最大值结果,可以解释为作者自定的汇编接口;若要让C按 unsigned max(unsigned,unsigned) 调用,应改成R0返回。调用者、被调者只要各自猜不同接口,程序就会出错。
第七章 · 第24节
嵌套调用与幂函数:别把计算结果也恢复掉
例 7.46、栈图 7.5—7.11;第 33—36 页
掌握要求:能追踪主程序→Pow→Mul→返回;保护LR与需保留寄存器,但不误恢复输出R0。
背景为什么要讲:这是本章最值得认真改正的例子,单纯把所有寄存器压栈并不能保证程序正确。
原书Pow把R0—R7、LR共9个字压栈,末尾又把R0—R7恢复:即使R0中算出了幂,也被旧R0覆盖。同时9个字=36字节,会把原来8对齐的SP变成4模8;再BL到公开接口不合适。指数=1的路径若不先退出,还可能把计数减到−1继续循环。
下面使用清晰接口:R0=底数,R1=非负指数;返回R0=低32位结果。指数0返回1,包括按本例约定的0的0次方;溢出按模 保留低部。
Pow32 PROC
PUSH {R4-R6,LR} ; 16字节,保护原链接并维持8对齐
MOV R4,R0 ; 底数跨调用保留
MOV R5,R1 ; 剩余次数跨调用保留
MOVS R0,#1 ; 累乘器也是返回值
PowTest
CMP R5,#0
BEQ PowDone
MOV R1,R4
BL Mul32 ; R0 <- R0 * R1,可改R0-R3/R12
SUBS R5,R5,#1
B PowTest
PowDone
POP {R4-R6,PC} ; 不恢复R0,返回计算结果
ENDP
Mul32 PROC
MUL R0,R0,R1
BX LR
ENDP
R6在本函数未承载算法数据,这里与R4/R5/LR一起保存是为了让帧为16字节并保持对齐;也可以用明确的栈填充来实现。栈空间不是无限的,实际工程要确保帧和嵌套深度不会越界。
对于原书 STMFD SP!,{R0-R7,LR},地址布局可用上一章规则核对:若旧SP=0x20000460,新SP=0x2000043C;最低地址保存R0,最高的0x2000045C保存LR。不需要臆测“CPU内部哪一个先写”,编号到地址的映射才是可依赖的结果。
主程序需要跨调用保留的R0—R3临时值,由主程序自己先挪到安全位置或压栈;被调函数借用的R4—R11等,按ABI恢复。不要一律恢复所有寄存器,否则连返回值都没了。
第七章 · 第25节
阶乘:先把0!、1!、上限和返回值说清
习题8—9;第 48 页;承接7.3.3
掌握要求:能独立写0≤N≤10的迭代阶乘函数,说明不变量和边界;读懂递归的额外栈代价。
背景为什么要讲:原章练习直接要求阶乘,正文没有完整展开,这里补足写题所需算法。
定义:0!=1,N!=1×2×…×N。题目“N≤10”还应补上N为非负整数,否则负数阶乘没有这里的整数乘积含义。本章使用无符号参数,超出10返回0作为约定的错误标记,正常结果从1开始不会与它冲突。
Fact10 PROC
CMP R0,#10
BHI FactBad
MOV R1,R0 ; n
MOVS R0,#1 ; acc
FactTest
CMP R1,#1
BLS FactDone
MUL R0,R0,R1
SUBS R1,R1,#1
B FactTest
FactDone
BX LR
FactBad
MOVS R0,#0
BX LR
ENDP
不变量可理解为:每轮acc保存已经乘过的高端因子,R1指向下一项;R1≤1时剩余因子不会改变结果。N=0或1一开始就返回1;N=5依次得到1→5→20→60→120;10!=3628800=0x00375F00,放得进32位有符号整数。
12!=479001600还放得进有符号32位,但13!已经超过无符号32位;这些背景只用来理解“为什么要说结果宽度”。原题限定N≤10,按这个范围写对就够,不要求自行实现任意精度。
递归可以写成n×fact(n−1),但每一层都要保存自己的n和返回链接。例如在合法输入前提下:先判断n≤1,再PUSH {R4,LR},把n存R4,R0减1后BL自身,返回后MUL R0,R0,R4,POP {R4,PC}。R4/LR每层8字节,n=5到基本情形需要4个非基本帧。迭代没有递归深度负担,基础题优先掌握迭代,不必为了“像数学定义”强行用递归。
第七章 · 第26节
ATPCS/AAPCS:寄存器并不是谁想改就改
7.4.1;第 37—38 页;习题2、7—10
掌握要求:会分caller-saved/callee-saved、参数/返回值/临时寄存器;不会依赖调用后仍保留的R0—R3或标志。
背景为什么要讲:ATPCS是原书使用的历史标准名;本章实际按兼容的AAPCS32基本整数接口解释,不能把不同ABI随便混链。
ATPCS(Arm-Thumb Procedure Call Standard)规定子程序间如何使用寄存器、栈和参数。AAPCS(Procedure Call Standard for the Arm Architecture)属于Arm ABI的一部分,对这些接口作了标准化发展。读原书认识ATPCS;写工程则让编译器、汇编模块和库采用同一套目标ABI及选项。
| 寄存器 | 基础接口角色 | 调用后可以假定仍保持原值吗? |
|---|---|---|
| R0—R3 | 前几个简单整数/指针参数、返回值与临时量 | 不可以 |
| R12/IP | 临时量,链接跳板等也可能使用 | 不可以 |
| R4—R8、R10—R11 | 被调函数保存的变量寄存器 | 被调函数修改就要恢复 |
| R9 | 平台指定角色;某些约定中也是保存寄存器 | 按目标平台ABI,基础练习不借用 |
| SP/R13 | 栈指针 | 函数返回时恢复进入值 |
| LR/R14 | 返回链接;再次BL会覆盖 | 需要继续返回的函数自行保护 |
| PC/R15 | 控制执行位置 | 不当作通用数据临时量 |
“caller-saved”并不是说每次BL前都把R0—R3无脑压栈,而是调用者若还要那些旧值,自己负责保住。“callee-saved”也不是每个函数都压R4—R11,而是被调者用了其中哪个就恢复哪个。原书a0—a3等别名不作为必背项,不同文档别名有差别,寄存器编号与职责最可靠。
条件标志通常不跨普通函数调用保持;不要 CMP R0,#0; BL Other; BEQ Label,期待BEQ仍用调用前CMP结果。链接器产生的长跳转/互操作跳板可能借用R12,也不该把重要值跨调用放R12。
M3/M4的Thumb-2可以操作高寄存器;原书“Thumb通常只能使用R4—R7”是早期编码便利性的背景,不是M4永远不能用R8—R11。使用高寄存器仍受具体指令编码与ABI约束。
第七章 · 第27节
第五个参数与8字节对齐:先画入口SP,再算偏移
7.4.1 参数与栈;例 7.50;第 38、41—42 页
掌握要求:会布置5个简单32位参数,计算被调函数压栈后参数的新偏移;知道PRESERVE8不自动调整SP。
背景为什么要讲:原例只压一个参数字后直接BL,破坏了8字节接口对齐,需要修正。
限定参数全是普通32位整数:前4个依次放R0—R3;第5个在被调函数入口SP所指的栈位置。更多字参数继续按ABI放在更高地址,不应把C参数列表简单当作任意push顺序口诀。
MOVS R0,#1
MOVS R1,#2
MOVS R2,#3
MOVS R3,#4
MOVS R4,#5
SUB SP,SP,#8 ; 4字节参数 + 4字节对齐空位
STR R4,[SP] ; 第5个参数位于入口[SP]
BL Sum5
ADD SP,SP,#8 ; 调用者回收参数区
假设调用前SP已经8对齐,这样BL时仍对齐。如果这段属于可返回的函数,还要保护其原LR及修改的R4,代码片段之外也得遵守接口。不能因为只展示参数区就漏掉外层帧。
若Sum5一进入就 PUSH {R4,LR},新SP=入口SP−8,第5个参数现在位于 [新SP,#8];若压3个字,偏移是12。永远从实际保存字节数推偏移,不背某个编译器截图里的固定#12。
AAPCS32的通用栈约束要求至少4字节对齐;公开调用接口还要求8字节对齐。初学时可用更容易执行的纪律:所有向外BL前都维持8对齐,返回时SP回到进入值。满递减栈向低地址增长,SP指活动栈顶;FD/ED/FA/EA四种模型在上一章已讲,原章这里认识FD为采用的模型即可。
PRESERVE8 是对工具声明本模块保持相应约束,不是会在CPU上执行的对齐操作。原书SP=0x20000460,再STR一个字到[SP,#−4]!后SP=0x2000045C,已经不是8对齐;只写PRESERVE8无法修复它。
第七章 · 第28节
不止“前四个进寄存器”:类型、返回值和C++名字
7.4.1 参数/结果规则;7.4 C/C++;第 36—38 页
掌握要求:简单整数接口能独立实现;遇到64位、浮点或结构体知道必须先核对ABI,不能套“4个参数”的口号。
背景为什么要讲:这些扩展在基础习题不必写复杂实现,但要知道简化规则的适用边界。
本章题目的unsigned/int/指针通常是32位,一个参数用一个核心寄存器,所以“前四个”成立。若参数是64位数,可能需要对齐的寄存器对;结构体可能拆分或通过地址间接传递,不能再按“第几个人类参数”机械分配。
例如基础AAPCS32下 f(int a, long long b, int c):a用R0,b要求偶数编号寄存器对,使用R2/R3,R1可能空出,c因此走栈。不是a→R0、b→R1、c→R2。 这个例子只用于提示类型影响分配。
| 返回类型/配置 | 需要认识的规则 |
|---|---|
| 简单32位整数、指针 | R0 |
| 64位整数 | 核心寄存器对R0/R1,字序按ABI |
| 更复杂的结构/大结果 | 常需要调用者提供结果区及隐藏地址参数,依具体类型 |
| 浮点,基本/softfp接口 | 通常按基本核心寄存器规则传参/返回 |
| 符合VFP参数变体的hard-float接口 | 合格浮点类型使用相应S/D寄存器,具体类型仍要匹配 |
M4有FPU不等于所有浮点参数自然走S0;-mfloat-abi等选项和库的接口必须匹配。原书f0/d0/s0混列包含旧浮点模型,不把它作为M4统一规则。
C编译后的外部符号通常与函数名对应;C++为了重载可能修饰名字。让C++调用普通汇编函数可声明:
extern "C" unsigned asm_strlen(const char *s);
普通C文件写 extern unsigned asm_strlen(const char *s);;函数声明里的extern通常可以省略,它不替代汇编端的EXPORT。extern "C"改变语言链接约定,不会自动修复错误的参数/返回值类型、栈布局或寄存器保护。
第七章 · 第29节
C调用汇编:字符串复制与求长度完整走一遍
例 7.48;习题10;第 38—40、48 页
掌握要求:从C原型确定R0/R1,按字节读到零;复制包含终止符,长度不包含终止符。
背景为什么要讲:指针就是地址,C与汇编共享同一块内存,不需要神秘的“语言转换”。
原例 strcopy(dst,src) 的第1参数dst在R0,第2参数src在R1。源串必须可读且有终止符,目标必须有足够可写空间,两个区域不能按此算法随意重叠。
EXPORT strcopy
strcopy PROC
CopyLoop
LDRB R2,[R1],#1
STRB R2,[R0],#1
CMP R2,#0
BNE CopyLoop
BX LR
ENDP
注意“先写再判断”:把0也复制过去,否则目的区可能不是合法C字符串。这个自定义函数返回void;它推进R0,不按标准strcpy的“返回原dst”接口承诺。若要改成标准风格,必须保存原dst并在R0返回。
求长度不同:遇到0就退出,不把0计入。空字符串第一个字节就是0,长度为0;不能先加1再无条件当作结果。
EXPORT asm_strlen
asm_strlen PROC
MOV R1,R0 ; 指针工作副本
MOVS R0,#0 ; 长度,也作为返回值
LenLoop
LDRB R2,[R1],#1
CMP R2,#0
BEQ LenDone
ADDS R0,R0,#1
B LenLoop
LenDone
BX LR
ENDP
本章32位长度实现要求长度可表示且指针有效、有可达终止符;不是通用的坏指针检测函数。中文UTF-8里一个汉字可能占多字节,此函数数的是字节,不是汉字个数。原例英文字母字符串的字节数与字符数恰好相同。
C端类型、实参顺序与汇编出口要一起检查。const char *src表示不通过该指针写源串;目的数组如 char dst[20] 必须真的有20个字节。第10题给出C调用模块、汇编实现、启动与结果观察点。
第七章 · 第30节
汇编调用C:先把自己变成合格的调用者
例 7.49—7.50;习题9;第 40—42、48 页
掌握要求:会IMPORT函数、建立参数、保持栈与LR,BL后从R0取值;调用后不依赖易变寄存器旧内容。
背景为什么要讲:C函数的内部寄存器分配会随优化改变,不能把某张反汇编截图当固定实现。
unsigned sum2(unsigned a, unsigned b) { return a + b; }
unsigned sum5(unsigned a, unsigned b, unsigned c,
unsigned d, unsigned e) { return a+b+c+d+e; }
sum2调用前R0=1、R1=2,BL后R0=3。它可能被编译成一条加法再返回,也可能有更多移动与栈操作;只要遵守接口,调用者不应该关心它究竟借了R2还是R3。
sum5调用前R0—R3=1、2、3、4,[入口SP]=5,结果R0=15。第27节的8字节参数区布置才是修正后的调用。调用者回收自己的出参区,函数恢复自己的保存帧,各自责任不要混。
第9题C阶乘函数只用参数和局部变量,没有标准库依赖:
unsigned factorial_c(unsigned n)
{
unsigned acc = 1;
if (n > 10u) return 0u;
while (n > 1u) {
acc *= n;
--n;
}
return acc;
}
汇编端用 IMPORT factorial_c,令R0=N,BL后取R0。若是在一个可返回的汇编函数中,先PUSH {R4,LR},调用后POP {R4,PC},不恢复R0覆盖结果。
原书先随意把SP设成RAM某地址,教学上能够演示,但完整工程应从实际预留的栈得到SP。对C函数还需核对:是否访问需初始化的全局数据、是否调用运行库、浮点接口是否一致。“C函数能编译”不表示不需要任何运行环境。 本章最小工程只使用已明确满足这些条件的简单C函数;一般程序接厂商启动和运行库更合适。
第七章 · 第31节
内联汇编、嵌入式汇编函数与独立.s文件
7.4.2;7.4.3;例 7.51;第 42—47 页;习题11
掌握要求:认识原书两种嵌入形式;知道支持情况取决于编译器与版本;能读一个正确约束的GNU风格小例子。
背景为什么要讲:本节必须讲,因为原书“M4只能用第二种”的绝对化说法会让你误判工具错误。
原书的分类建立在旧Arm Compiler/armcc语境:
| 形式 | 典型外形 | 谁管理接口 |
|---|---|---|
| 内联汇编块 | C函数内部 __asm { ... } |
编译器把局部变量与汇编结合,按该工具的限制分配 |
| 嵌入式汇编函数 | __asm unsigned func(...) { ... } |
函数体由汇编实现,作者遵守参数、保存、返回规则 |
| 独立汇编源文件 | .s加EXPORT/IMPORT |
通过链接与ABI接到C |
旧armcc的两种 __asm 写法和GCC/armclang的GNU扩展汇编不是同一语言。Arm Compiler6的armclang采用GNU风格内联汇编,不能把armcc的花括号示例原样粘过去。Arm官方迁移说明解释了这一差别。
GNU风格的一个小函数(GCC/armclang目标为M3/M4)如下:
static unsigned add1(unsigned x)
{
unsigned y;
__asm__ volatile ("adds %0, %1, #1"
: "=r"(y)
: "r"(x)
: "cc");
return y;
}
%0/%1是编译器安排的输出/输入操作数;=r表示寄存器输出,r表示寄存器输入;cc声明条件标志被修改。这个单指令例子允许输出与输入同寄存器。多指令序列若过早写输出、之后还要读输入,可能需要early-clobber等约束,不能照搬这个最小模板。
访问C未显式列出的内存可能需要memory clobber或精确的内存操作数;volatile不等于自动告诉编译器所有内存都变了,也不等于处理器内存屏障。长循环和函数调用优先放独立.s函数,更容易把接口审清楚。不要在普通C内联块里随意改SP、埋BX LR或假定R0正好对应某个局部变量。
原例7.51用内联汇编实现复制:其算法仍是读一个字节、写一个字节、指针推进、直到0。第29节已经给出了同一算法的独立汇编版,便于逐步验证。原书那些“不得直接写PC”“避免物理寄存器冲突”等限制应放在相应编译器内联汇编模式下理解,不是对所有独立.s函数的统一禁令。
关于第11题:早期armcc在Thumb下的内联汇编支持受限,故M3/M4常使用嵌入式汇编函数;原书第47页又说明armcc5.01/MDK4.60起支持相应Thumb内联形式。因此M4硬件本身并没有禁止内联汇编,能用哪种由工具链版本与语法决定。 回答时给出旧教材条件,同时补上这个限定,不把过时条件背成架构定律。
第七章 · 第32节
汇编访问C全局变量:先拿地址,再访问内容
7.4.2 第2部分;第 44 页
掌握要求:会IMPORT一个外部对象并用LDR/STR访问;理解共享变量的可见性与并发问题。
背景为什么要讲:原书这里只提了一句话,补上地址和值的连接,混合编程才完整。
C端定义:
volatile unsigned ticks; /* 真正定义对象,有存储 */
汇编端引用:
IMPORT ticks
LDR R1,=ticks ; R1是地址
LDR R0,[R1] ; R0才是ticks的值
ADDS R0,R0,#1
STR R0,[R1]
若写成 extern volatile unsigned ticks; 是声明外部对象,仍需某处唯一真正定义它。C端若用static把对象限制在文件内部,其他目标文件不能简单靠同名IMPORT访问。C++还要考虑语言链接名字。
volatile让相应C访问具有所需的可观察性,但不把“读—加—写”三条指令合成原子操作。若中断也更新ticks,两边可能覆盖;用适当临界区、原子机制或平台提供的同步方法。这里提并发是为了不把示例误当万能计数器,不要求现阶段实现完整OS同步。
全局对象的初始值要靠启动运行环境建立。最小启动练习中的Result会先写后读,不依赖RAM随机值;一般C工程则按编译器/启动库规则处理.data和.bss。
第七章 · 第33节
CMSIS到底统一什么:核接口、RTOS接口、DSP库
7.4.3 第1部分;图 7.19;第 45 页
掌握要求:能解释CMSIS层次与三个原章组件;知道它不自动统一各厂商所有外设寄存器。
背景为什么要讲:它让特殊指令与核寄存器访问不必处处写工具专用汇编,是后续C开发的基础。
CMSIS全称Cortex Microcontroller Software Interface Standard。原章重点的三块内容:
| 组件 | 作用 | 不要误解成什么 |
|---|---|---|
| CMSIS-Core | 核寄存器定义、核外设与处理器访问函数、异常名/启动约定等 | 不是所有厂商GPIO/UART完全一样 |
| CMSIS-RTOS接口 | 给RTOS服务定义共同API,便于组件对接 | 接口标准本身不是一个唯一RTOS实现 |
| CMSIS-DSP | 常用信号处理算法与数据类型支持 | 不因包含头文件就自动替你设计滤波算法 |
芯片厂商设备头文件通常描述具体芯片及其外设,配合CMSIS-Core。应用既可以通过厂商驱动,也可以按设备寄存器定义控制硬件。系统若使用RTOS,应用还通过其API获得线程、同步等服务。
原书“超过60个DSP函数”与列出的旧头文件名是当时库版本的背景;不要背成永久数量或唯一文件组织。实作时通常包含目标设备头文件,由设备包接入合适的CMSIS头与编译器适配;不要凭书里的拼写猜 core_c4_simd.h 然后生造文件。
本章做题只需能说明层次,并在特殊寄存器访问时选用规范接口;暂时不要求移植RTOS或完成DSP项目。这些内容保留是为了让你理解“C里能调用__WFI/读取PRIMASK”背后是谁提供了接口。
第七章 · 第34节
内在函数、内联汇编、习语识别:C怎样产生特殊指令
7.4.3 第2部分;第 46—47 页
掌握要求:认识原章全部四类途径;会选CMSIS接口并区分WFI/WFE、屏障、编译器重排。
背景为什么要讲:普通ISO C表达式没有“读PRIMASK”“执行WFI”的统一语法,需要目标相关接口。
原章提到的途径全部保留:CMSIS访问/内在函数、编译器内建函数、内联或嵌入汇编、编译器关键字/属性与习语识别。它们有的表现为C函数调用,有的由编译器直接替换成指令;看到函数样子不等于真的一定产生BL函数调用。
| 常见CMSIS接口 | 含义 | 学习深度 |
|---|---|---|
__NOP() |
无操作提示 | 认识,不用它随意构造精确延时 |
__WFI()、__WFE()、__SEV() |
等中断、等事件、发事件 | 了解唤醒条件与具体系统配置有关 |
__DMB()、__DSB()、__ISB() |
内存访问顺序、完成、指令同步 | 沿用第六章区分,不混作普通延时 |
__get_PRIMASK()等 |
读核特殊寄存器 | 看寄存器作用与权限 |
编译器builtin、旧armcc的__svc等 |
目标工具提供的特殊功能 | 查对应编译器文档,不作为通用C语法 |
GCC/armclang可用 __asm__ volatile("svc #0" ::: "memory"); 表达一个特定工具语境下的SVC,但仅有这一行并没有实现系统服务;还要配置并实现异常处理。一般优先使用已有平台接口,避免把服务号和权限模型藏在不明汇编里。
“习语识别”是编译器识别某种C计算模式,生成合适的机器指令;不保证任何写法、优化级别或版本都生成同一条指令。对性能/同步有要求时查看实际反汇编,并按工具保证选择内建接口。
volatile、编译器内存屏障、CPU的DMB/DSB/ISB分属不同语义,不能互相无条件替代。CMSIS对不同编译器提供适配,但也要看目标支持和版本;“内建函数永远最优、CMSIS永远较慢”不是可靠定律。
第七章 · 第35节
特殊寄存器:临界区恢复旧状态,Q清除读懂即可
7.4.3;表 7.2;第 46—47 页
掌握要求:能解释PRIMASK临界区为何恢复旧值;认识所有列出的寄存器组及读写限制;知道命名寄存器变量是工具特性。
背景为什么要讲:这些寄存器影响权限、栈与异常,提供用途和边界,避免把字符串寄存器名当作任意可写变量。
CMSIS常见写法:
#include "目标设备头文件.h" /* 用实际设备包提供的文件名 */
void update_shared(void)
{
unsigned old = __get_PRIMASK();
__disable_irq();
/* 在特权、合适上下文中执行短小的共享数据更新 */
__set_PRIMASK(old);
}
头文件行是说明性占位,实际源码必须改成例如设备包要求的对应设备头文件。这个片段强调恢复进入前状态;是否还需特定屏障等同步由真实系统操作决定,不能把它当所有设备的万能模板。
如果进入前PRIMASK已为1,结尾直接 __enable_irq() 会擅自打开原来被关闭的可配置中断;恢复old才支持这种嵌套使用。PRIMASK也不屏蔽NMI和HardFault,不能由此声称“所有异常都不可能发生”。
原表7.2的全部名称可以按组记:
| 组 | 原表所列 | 怎么理解 |
|---|---|---|
| 运算状态 | APSR | NZCVQ等,按支持字段访问 |
| 异常/执行状态及组合视图 | IPSR、EPSR、IAPSR、EAPSR、IEPSR、PSR/XPSR | 不是一堆彼此独立的普通可写寄存器;IPSR只读,EPSR经普通MRS不可读出其执行字段、MSR不能随意改执行状态 |
| 优先级/异常屏蔽 | PRIMASK、BASEPRI、BASEPRI_MAX、FAULTMASK | 写入有权限与目标限制;BASEPRI_MAX用于只增强相应屏蔽效果 |
| 栈与控制 | MSP、PSP、CONTROL | 栈切换与权限,不能在普通C函数随意改SP后继续假定原栈仍有效 |
| 通用/特殊用途编号 | R0—R12、R13/SP、R14/LR、R15/PC | 受ABI及编译器分配约束,不是随便绑定后就安全 |
BASEPRI里的优先级数值越小通常表示优先级越高;“MAX”并不表示简单取无符号数值最大值,而是只增强屏蔽。BASEPRI=0表示不施加这个阈值屏蔽。其编码还涉及芯片实际实现的优先级位,本章不要求写一套中断管理器。
旧armcc的命名寄存器变量示意为:register unsigned reg_apsr __asm("apsr");,原书漏失的下划线与关键字不能照抄。此特性不是标准C;GCC中把变量绑定寄存器的机制也不能当作等价的特殊寄存器访问保证。普通应用优先用CMSIS/Core访问接口。CMSIS寄存器访问说明可核对名称与权限。
Q是黏性饱和标志,位27。0xF7FFFFFF就是清bit27的掩码。若在独立汇编中要清Q、保留同次读到的NZCV,可以按目标支持使用:
MRS R0,APSR
BIC R0,R0,#0x08000000
MSR APSR_nzcvq,R0
这同时写回相应标志字段,不是能随意写完整xPSR。若从C调用,普通C运算/函数调用本来就可能改变算术标志,不能把这个片段当作跨任意C代码“保存所有标志”的承诺。基础习题只需要理解清哪一位、为何要用MRS/MSR或相应工具接口,命名寄存器变量的完整限制以查读为主。
第七章 · 第36节
做题和调试都用同一张检查单
7.1.2、7.3—7.4;原章11道习题;第 5—8、24—48 页
掌握要求:从需求写到可核对结果,发现错误时能定位到数据、条件、循环、调用或启动层。
背景为什么要讲:把工具与算法接回同一条学习路线,防止“看懂答案”被误当成“能够自己写”。
拿到题先写一行规格,例如:“输入是10个有符号32位数;输出R0;只统计严格大于0;不改数组。” 这一行会替你决定LDR宽度、步长、比较条件与返回位置。
然后做六步:
- 列寄存器职责:输入/指针、计数、当前值、累加/输出,不让同一寄存器同时承担冲突角色。
- 用C式伪代码或流程图明确路径,特别是0次、1次、最后一项。
- 选指令与条件:有无符号、是否需要更新标志、哪一次比较决定哪次跳转。
- 补数据与工程:段、EXPORT/IMPORT、函数/程序入口、栈、结果空间、执行结束路径。
- 手算一个小样本,再用边界样本检查:0、1、相等、负数、空串、数组最后一项。
- 调试核对R0/R3、指针、剩余数、SP/LR、结果内存;若出错只缩小到一轮/一次调用,不盲目重写全部。
| 现象 | 先查什么 |
|---|---|
| 汇编器不认识AREA、EXPORT | 汇编方言与工具选择 |
| 链接说找不到函数 | 是否加入定义文件、名字/大小写、EXPORT/IMPORT、C++修饰 |
| 一运行就异常 | 向量、Thumb入口、SP、真实存储区、非法指令/地址 |
| 循环多一次或死循环 | 初值、入口0次判断、更新、比较方向、计数下溢 |
| 函数里算对,返回后值又错 | 输出被POP覆盖、LR未保护、caller-saved值被改 |
| 第5参数总不对 | BL时SP对齐、入口SP与压栈后SP的偏移 |
| 内存值对,R0却是地址 | 少了一次LDR,或把Result地址当返回值 |
下面的11张原题卡完整保留题意和解题要求,带完整应用模块的题都配好共同启动文件和源码。先独立写,再展开答案;若只会沿着注释阅读,还没有达到本章过关标准。 本章背景已经全部交代:旧开发工具、冷门伪指令、表达式、浮点ABI、CMSIS和工具特性都能在目录中找到;不要求你把它们全部默写成产品工程。
原书内容逐项对照
全部小节与51个例子的去处
| 原书 | 覆盖内容 | 讲解 |
|---|---|---|
| 7.1.1 | SDT、ADS、RVDS、MDK、GCC、DS-5、表7.1 | 02、03 |
| 7.1.2 | 开发周期、μVision、模拟器与调试探针 | 02、03、36 |
| 7.2.1 | GBL/LCL、SET、RLIST;例7.1—7.4 | 05 |
| 7.2.2 | 全部数据定义、U变体、SPACE、MAP/FIELD;例7.5—7.13 | 06、07、08 |
| 7.2.3 | IF、WHILE、MACRO、MEXIT;例7.14—7.16 | 09、10 |
| 7.2.4 | 全部13类其他伪指令;例7.17—7.28 | 05、11、12、13、14 |
| 7.2.5 | 变量/常量、命名、代换与EQU;例7.29—7.31 | 05、15 |
| 7.2.6 | 全部数字/逻辑/字符串/寄存器表达式;例7.32—7.35 | 15 |
| 7.3.1 | 语句格式、标号、大小写 | 04 |
| 7.3.2 | 代码/数据段、Num/Nums与程序启动 | 11、16、17 |
| 7.3.3 顺序 | 例7.36寻址、7.37双字加法 | 17、18 |
| 7.3.3 分支 | 例7.38—7.43、B/BL/BX、条件与状态 | 13、19、23 |
| 7.3.3 循环 | for/while、7.44求和、7.45冒泡 | 20、21、22 |
| 7.3.3 子程序 | 栈图7.5—7.11、7.46幂与嵌套、7.47最大值 | 23、24、25 |
| 7.4.1 | ATPCS/AAPCS、栈/参数/结果、例7.48—7.50 | 26、27、28、29、30 |
| 7.4.2 | 内联/嵌入汇编、7.51复制、共享全局变量 | 29、31、32 |
| 7.4.3 | CMSIS、四类途径、命名寄存器、表7.2、清Q | 33、34、35 |
| 习题1—11 | 全部题意、答题规则、完整应用文件与边界样本 | 逐题 → |
例1—35按伪指令与表达式主题合并解释,36—51的算法及混合调用分别展开。旧IDE截图改为窗口职责,栈图改为可变阶段SVG;修正笔误并保留用途与掌握边界。
先算,再展开
十二道综合短自测
N EQU 10、N SETA N+1、ADDS R0,#1分别在哪个阶段起作用?N能在未声明为可变汇编变量时用SETA改吗?- 地址0x1001先DCB一个字节,再DCD一个字,DCD标号是多少?改DCDU怎样?
- MAP 0x100;A FIELD 16;B FIELD 32;S FIELD 256。B/S值和总描述长度是多少?有分配空间吗?
- 当前0x1003,ALIGN 8后多少?AREA的ALIGN=3是否代表每条指令8字节?
- R2=−2、原R3=−1,第3题的有符号答案是多少?若用BHI会走哪条路?
- 倒计数循环初值为0却直接进循环,会发生什么?十个32位元素最后一次正确LDR从首址加多少?
- 降序冒泡从左向右一轮后固定到右端的是最大还是最小?五元素要比较几轮、几次?
- 函数PUSH {R0-R7,LR},算出R0,最后POP {R0-R7,PC}。两个主要问题是什么?
- 入参SP=0x20001000;被调函数PUSH {R4,LR}后,第5个简单32位参数在什么地址/新SP偏移?
strcopy(dst,src)的R0/R1是谁?"ARM"复制几字节、长度函数返回多少?- 进入临界区前PRIMASK=1,退出时直接enable_irq有什么问题?
- “M4不支持内联汇编”“PRESERVE8会帮CPU调整SP”“INCBIN一个.o就是链接”分别哪里错?
展开答案与过关要求
- EQU/SETA在汇编时处理;ADDS运行时修改寄存器与标志。普通EQU常量不能用作反复修改的SETA变量,应先GBLA/LCLA声明相应可变类型。
- DCB占0x1001,接下来当前位置0x1002。DCD先补到0x1004,标号0x1004;DCDU则从0x1002开始,但访问合法性要另查。
- B=0x110,S=0x130;描述长度16+32+256=304=0x130字节,结束0x230;不实际分配。
- 0x1008,填5字节。AREA ALIGN=3仅约束段起点按8对齐。
- −1>−2,结果8。按无符号位串,0xFFFFFFFF>0xFFFFFFFE也为真,因此这一个样本BHI恰好同路;要区分符号规则,可用原R3=1、R2=−2,此时有符号为大于、无符号为不大于。测试不能只选恰好一致的数据。
- 0减1下溢为0xFFFFFFFF,可能跑极多轮/越界;第10项下标9,LDR地址首址+36,读取到+39。
- 最小;4轮,比较4+3+2+1=10次。
- R0计算结果被旧R0恢复覆盖;36字节帧使原8对齐SP在进一步公开调用时不再8对齐。还要检查是否需保护这些寄存器,以及算法自身边界。
- 参数仍在原入口SP=0x20001000;新SP=0x20000FF8,因此是[新SP,#8]。
- R0=dst地址,R1=src地址;复制A/R/M/0共4字节,长度返回3。
- 会把本来关闭的可配置中断打开,破坏外层状态;保存并恢复old更合适。
- 内联支持由编译器/版本决定;PRESERVE8是声明,不执行调整;INCBIN只是原样嵌字节,不解析目标文件符号与重定位。
过关要求:第2、4—10题应能独立手算或写出核心代码;第1、3、11—12题要能解释层次和限制。随后独立完成11张原题卡,不能只在展开答案后觉得“看懂了”。
第48页全部11题
原题答题卡与完整程序
程序题使用第16节共用启动文件与各题全部应用文件,组成完整学习工程;源码包包含同样内容及链接布局、配置说明。每题分开构建。
习题7.1 / 汇编与C各有什么特点
汇编语言和C语言相比,各有什么特点?
展开解题过程与代码
汇编接近指令与寄存器,便于精确控制特殊操作、启动与局部关键代码;但可读性、维护和跨架构移植通常较差,写对现场/接口需要更多人工工作。C的结构、类型、库和工具支持较好,通常更易维护与移植;涉及特殊指令/寄存器时仍需要平台接口、内建函数或汇编。
不要答成“汇编一定更快,C一定不能访问硬件”。 C可以通过volatile映射寄存器、CMSIS和厂商库访问硬件;优化编译器也常能生成很好的代码。选择应看功能、可验证性和实际测量。
习题7.2 / ATPCS规定了什么
什么是ATPCS标准?
展开解题过程与代码
ATPCS是Arm-Thumb Procedure Call Standard,规定Arm/Thumb子程序调用接口,包括寄存器使用、数据栈、参数传递等基本规则,使单独编译和不同语言编写的函数能正确配合。
答题至少写出:简单参数通常经R0—R3,简单32位结果经R0;易变寄存器由调用者按需保护;被调者修改需保留寄存器则恢复;SP和返回链接正确,栈遵守所选ABI约束。补充:实际工程还要核对AAPCS32及平台/浮点等变体,不能把ATPCS与不同ABI的目标文件任意混合。
习题7.3 / 按原R3与R2比较后赋值
编写完整程序:若R3>R2,将R2+10放R3,否则将R2+100放R3。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。 此解按有符号比较,输入通过开头两条MOVS设置;运算结果按32位保留。 如果题目规定无符号,把BGT改为BHI。比较必须发生在改写R3之前。
PRESERVE8
THUMB
AREA AppCode,CODE,READONLY,ALIGN=2
EXPORT exercise_main
exercise_main PROC
MOVS R2,#5 ; test input
MOVS R3,#8 ; original R3
CMP R3,R2 ; signed comparison of original values
BGT Greater
ADD R3,R2,#100
B StoreResult
Greater
ADD R3,R2,#10
StoreResult
LDR R1,=Result
STR R3,[R1]
BX LR
ENDP
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
EXPORT Result
Result SPACE 4
END
默认5、8得到15;原R3=5与R2相等时走else,得到105;R2=−2、原R3=−1时走大于路径,得到8。相等不能误走“>”。Result另存一份R3便于观察,R3本身仍保留答案。若需数学意义上无溢出的有符号和,还要限定R2范围。
习题7.4 / 统计十个数里的偶数
将数据段10个数据中的偶数个数统计到R0。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。 本解每项是32位整数,最低位为0即为偶数。0和负偶数也计入,数组不改写。
PRESERVE8
THUMB
AREA AppCode,CODE,READONLY,ALIGN=2
EXPORT exercise_main
exercise_main PROC
MOVS R0,#0
LDR R1,=Values
MOVS R2,#10
CountLoop
LDR R3,[R1],#4
TST R3,#1
BNE NotEven
ADDS R0,R0,#1
NotEven
SUBS R2,R2,#1
BNE CountLoop
LDR R1,=Result
STR R0,[R1]
BX LR
ENDP
AREA Inputs,DATA,READONLY,ALIGN=2
Values DCD -4,-3,0,1,2,3,4,5,6,7
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
EXPORT Result
Result SPACE 4
END
默认数据的偶数是−4、0、2、4、6,所以R0=5。R1每轮加4,R2从10减到0;只有偶数分支加计数,但两条分支都必须推进循环。结果还写到Result,不能把R0改成Result地址。
习题7.5 / 统计十个有符号正数
将数据段10个有符号数中的正数个数统计到R0。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。 严格大于0才是正数;用有符号条件,不能把负数当成大无符号值。
PRESERVE8
THUMB
AREA AppCode,CODE,READONLY,ALIGN=2
EXPORT exercise_main
exercise_main PROC
MOVS R0,#0
LDR R1,=Values
MOVS R2,#10
CountLoop
LDR R3,[R1],#4
CMP R3,#0
BLE NotPositive
ADDS R0,R0,#1
NotPositive
SUBS R2,R2,#1
BNE CountLoop
LDR R1,=Result
STR R0,[R1]
BX LR
ENDP
AREA Inputs,DATA,READONLY,ALIGN=2
Values DCD -4,-3,0,1,2,3,4,5,6,7
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
EXPORT Result
Result SPACE 4
END
默认数据中1—7共7个正数,所以R0=7。−4、−3和0不计。CMP后BLE跳过,余下才加计数;若用BHI/LS等无符号条件,会在负数上出错。
习题7.6 / 循环计算1至100的和
编写循环程序,实现1至100累加。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。
PRESERVE8
THUMB
AREA AppCode,CODE,READONLY,ALIGN=2
EXPORT exercise_main
exercise_main PROC
MOVS R0,#0 ; sum
MOVS R1,#100 ; next addend
SumLoop
ADDS R0,R0,R1
SUBS R1,R1,#1
BNE SumLoop
LDR R2,=Result
STR R0,[R2]
BX LR
ENDP
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
EXPORT Result
Result SPACE 4
END
R0为sum,R1为下一项;100、99、…、1各加一次。最后R0=5050=0x000013BA,R1=0,SP保持。固定N=100可直接进入;推广到允许N=0时,要在入口先检查0,避免先减到0xFFFFFFFF。公式100×101/2仅用于核对。
习题7.7 / 定义、调用和返回子程序
汇编程序怎样定义子程序?怎样调用?
展开解题过程与代码
给入口取标签,可配PROC/ENDP提供函数边界;按接口安排参数和结果;通过BL调用;正常叶函数用BX LR返回;非叶函数在嵌套BL前保护自己的LR,返回前恢复所需现场。
MOVS R0,#3
MOVS R1,#4
BL Add32 ; 返回R0=7
; 调用者继续执行
Add32 PROC
ADD R0,R0,R1
BX LR
ENDP
上面是调用片段和完整函数,不是独立上电工程;须放在有正确入口与结束路径的应用中。跨文件时定义端EXPORT、调用端IMPORT;同文件内部调用可以不导出。PROC不自动保存现场,ENDP也不自动产生BX。
习题7.8 / 汇编子程序计算N!
编写完整程序,用汇编子程序计算N!,N≤10。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。 补充前提:N为非负整数。此版本接受unsigned N,N>10返回0作为约定错误标记;0!和1!都返回1。主程序设置N=5。
PRESERVE8
THUMB
AREA AppCode,CODE,READONLY,ALIGN=2
EXPORT exercise_main
exercise_main PROC
PUSH {R4,LR}
MOVS R0,#5
BL Fact10
LDR R1,=Result
STR R0,[R1]
POP {R4,PC}
ENDP
Fact10 PROC
CMP R0,#10
BHI FactBad
MOV R1,R0
MOVS R0,#1
FactTest
CMP R1,#1
BLS FactDone
MUL R0,R0,R1
SUBS R1,R1,#1
B FactTest
FactDone
BX LR
FactBad
MOVS R0,#0
BX LR
ENDP
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
EXPORT Result
Result SPACE 4
END
Result与R0=120。PUSH {R4,LR}使外层原链接得到保护,帧为8字节;返回时POP {R4,PC}不覆盖R0。Fact10是叶函数,只改R0/R1和标志,无须额外保存R4等。自测N=0、1、2、10、11,期望1、1、2、3628800、0。
习题7.9 / 汇编调用C计算N!
编写完整汇编程序,调用C函数计算N!,N≤10。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。 工程还必须包含factorial.c,不是只写IMPORT就有函数。范围、0!与超范围标记同第8题。
PRESERVE8
THUMB
AREA AppCode,CODE,READONLY,ALIGN=2
EXPORT exercise_main
IMPORT factorial_c
exercise_main PROC
PUSH {R4,LR}
MOVS R0,#5
BL factorial_c
LDR R1,=Result
STR R0,[R1]
POP {R4,PC}
ENDP
AREA Results,DATA,READWRITE,NOINIT,ALIGN=2
EXPORT Result
Result SPACE 4
END
unsigned factorial_c(unsigned n)
{
unsigned acc = 1;
if (n > 10u) return 0u;
while (n > 1u) {
acc *= n;
--n;
}
return acc;
}
调用前R0=5,BL后R0=120。C可能改R0—R3/R12及标志,所以返回后重新取得Result地址。外层保存LR,SP在调用处8对齐;编译C和汇编时目标/ABI一致。这里C不访问需要启动初值的全局变量,不用库;一般C程序仍需完整运行库初始化。
习题7.10 / C调用汇编求字符串长度
C程序调用汇编函数计算字符串长度,并返回长度。
展开解题过程与代码
工程组成:共用第16节的 startup.s 和 layout.sct,加上下面列出的全部源文件。 本题与其他题分开构建,不能把多个exercise_main/Result同时链接。输入常量在只读数据段,结果在RAM先写后读;完整源文件也可从本讲义封面下载源码包。 本题加入main.c和strlen.s。函数接口为unsigned asm_strlen(const char*);输入R0指向有效、以0结尾的字符串,返回R0为不含终止符的字节数。
extern unsigned asm_strlen(const char *s);
volatile unsigned Result;
void exercise_main(void)
{
Result = asm_strlen("Hello ARM!");
/* Result = 10; initialization is not assumed before this write. */
for (;;) { }
}
PRESERVE8
THUMB
AREA StringCode,CODE,READONLY,ALIGN=2
EXPORT asm_strlen
asm_strlen PROC
MOV R1,R0
MOVS R0,#0
LenLoop
LDRB R2,[R1],#1
CMP R2,#0
BEQ LenDone
ADDS R0,R0,#1
B LenLoop
LenDone
BX LR
ENDP
END
默认"Hello ARM!"有10个字节,Result=10。空串返回0,"A"返回1。R1工作指针、R2读字节、R0累加;仅用caller-saved寄存器,因此叶函数直接BX LR。C调用后的R0可能随后被编译器重用,观测C程序最终值以volatile Result为准。本自定义启动调用exercise_main而非标准main,是为了明确最小运行前提;在厂商C模板里,可把函数内容接入main。
习题7.11 / M4能用哪一种嵌入汇编
C中嵌入汇编有哪两种方式?Cortex-M4能用哪种,为什么?
展开解题过程与代码
按旧教材术语:一种是C函数内的内联汇编块,一种是以__asm定义的嵌入式汇编函数。早期armcc不支持在Thumb下使用相应内联模式,而M4只执行Thumb,故该历史环境中使用嵌入式汇编函数。
完整限定必须补上:这不是M4硬件永久禁止内联汇编。 原书第47页已说明armcc5.01/MDK4.60起的相应Thumb内联支持;GCC/armclang又有GNU风格内联汇编。工具版本、语法和约束不同,不能把两种__asm花括号写法与GNU asm字符串直接互换。实际编程先查当前工具说明;独立.s加正确ABI也是清晰方式。
原书嵌入式复制例末尾应使用普通返回BX LR;BLX LR会重新写LR,不应作为这里的标准返回写法。
把原书的坑明确标出来
需要修正或加条件的表述
| 原书易误读处 | 修正后的理解 | 定位 |
|---|---|---|
| 旧工具“最新/最强”、固定支持表 | 原章历史背景;具体支持查版本、设备包与许可证 | 03 |
| 探针替代MCU、不占任何资源 | 常见探针控制真实芯片;调试/跟踪资源有限 | 03 |
| GBL/LCL定义运行时RAM变量 | 它们是汇编期变量,不分配运行时RAM | 05 |
| 例7.2变量名称错配;EQU与等号混用 | 声明、赋值名与类型对应;按本工具语法使用EQU/SET | 05、15 |
| DCQ每项8字节就自动8对齐;进制说明错乱 | 相应ArmASM定义通常字对齐;宽度、进制分开 | 07 |
| DCD/SPACE保证上电RAM已有初值 | 还看段属性、映像与实际启动初始化 | 07、16 |
| IF/WHILE当CPU分支;Counter不更新 | 它们控制汇编生成,汇编期循环也必须推进 | 09 |
| ALIGN=3 / ALIGN 3混同;ALIEN拼写;偏移公式 | AREA用指数,独立ALIGN用边界,位置形如o+k×b | 12 |
| CODE16表示所有Thumb都是2字节;M4能切A32 | M3/M4用THUMB,含2/4字节编码,不执行A32 | 13 |
| ENTRY就是复位;END让CPU停止 | 工具元数据/源文件结束不同于向量/运行路径 | 13、16 |
| INCBIN一个.o就是链接;ROUT限定RAM变量 | 嵌字节不解析目标符号;ROUT管数字局部标号 | 14 |
| ?LA恒为4 | 看生成长度,Thumb可为2字节 | 15 |
| 7.37高字在前等同普通小端uint64_t | 程序自定字序;每个字内部仍小端 | 18 |
| if/else写到不同寄存器;int与HI条件混用 | 统一输出,按类型选有/无符号条件 | 19 |
| 条件移位后无条件依赖旧CMP | 检查实际指令标志;明确入口测试、每轮重比 | 20 |
| 7.45降序目标、升序交换、尾后地址冲突 | 降序左<右才交换;首轮只比较n−1对 | 22 |
| 7.46保存所有寄存器就正确;指数1路径 | 恢复R0会抹掉结果;36字节影响对齐;先查边界 | 24 |
| PRESERVE8会调SP;第5参数只压一个字即可 | 声明不产生CPU调整;BL前实际8对齐并回收出参区 | 27 |
| 前4个人类参数各占一个寄存器;有FPU就用S0 | 类型、对齐、浮点ABI决定实际接口 | 28 |
| M4永远只能嵌入函数;BLX LR用作普通返回 | 工具版本决定内联支持;普通返回用BX LR等 | 31 |
| CMSIS-RTOS就是一个RTOS;旧头文件/单下划线 | 区分接口与实现,使用设备包的规范访问名 | 33、34 |
| 寄存器变量漏关键字;列出名字就都可任意写 | 工具专用语法,受权限、字段与ABI限制 | 35 |
这些修正都与“写出的程序是否真的符合自己的说明”有关。对照原书时优先按规格、地址、条件、边界与接口核对,而不是按错误注释推测处理器会迁就它。
本章学完应能做到
不看答案,把程序从规格写到结果
- 基本写题: 独立完成分支、十元素遍历、1—100求和、0—10阶乘与字符串长度。
- 现场与接口: 指出参数和结果位置,保护LR与需保留寄存器,算SP和第5参数偏移。
- 工程完整: 说明代码/数据、符号链接、向量、栈、初始化与结束路径,区分片段与完整工程。
- 边界检查: 会用相等、0、1、负数、空串与末元素测试,解释下溢、错误条件码和输出被恢复。
- 背景查读: 认识全部伪指令/运算符、历史工具、CMSIS与特殊寄存器,复杂ABI与工具方言会查。
建议分四次学:01—15建立层次;16—22练控制流;23—30练调用;31—36补工具接口并做全部习题。按“能自己写对一题”看进度,不按翻过多少页看进度。