
1. 这不是一次简单的“代码阅读”而是一场嵌入式系统遗产审计CMSIS-4 这个名字在 Cortex-M 开发者圈子里就像老式机械表里的游丝——看不见、摸不着却决定整块表走时准不准。它不是某个具体功能模块也不是一个能直接调用的 API 库它是 ARM 官方为所有 Cortex-M 芯片厂商、RTOS 厂商、编译器厂商、IDE 厂商共同铺设的一条“软件地基协议”。你写的裸机驱动、你移植的 FreeRTOS、你调试的 Keil 工程、你烧录的 STM32 固件……只要没绕开 CMSIS就一定踩在这块地基上。而 CMSIS-4正是这套地基在 2013 年左右定型、沿用至今最广泛、也最“沉默”的一代标准。它不像 CMSIS-5 那样拥抱 C、CMSIS-DSP 或 CMSIS-NN也不像 CMSIS-Core(M) 那样被拆解成更细粒度的组件它是一个完整、自洽、高度保守的静态工程体系——所有头文件、启动代码、设备外设访问宏、系统初始化函数全部以纯 C 源码形式提供不依赖任何构建系统不绑定特定 IDE不引入运行时库依赖。正因如此它成了无数量产产品、工业控制器、医疗设备固件的底层锚点。但问题来了当你的新项目要从 STM32F103CMSIS-4 黄金搭档迁移到 NXP i.MX RT1064默认支持 CMSIS-5或者你要把十年前的老项目移植到 RISC-V 平台又或者你接手一个没有文档的遗留工程第一件事不是写代码而是打开core_cm3.h和startup_stm32f10x_md.s逐行确认这个__NVIC_PRIO_BITS是不是被硬编码成了 4这个SystemCoreClock的计算逻辑是否和当前 PLL 配置匹配这个SCB-VTOR的重定向地址有没有被 bootloader 覆盖——这已经不是开发是考古。我做过三次完整的 CMSIS-4 工程尽调最长的一次花了 17 天只为确认某家医疗设备厂商的 2012 年固件中SysTick_Handler是否被意外重定义导致中断嵌套失效。CMSIS-4 的价值不在“新”而在“稳”它的风险不在“错”而在“隐”。它不报错它只是默默让HAL_Delay(1)变成 1.8ms让HAL_GPIO_WritePin()的上升沿延迟多出 3 个周期让整个系统在高温环境下出现偶发性看门狗复位。所以这篇评测不是教你“怎么用 CMSIS”而是带你亲手拆开这个“黑盒”看清它的焊点、铜箔走向、元件容差以及——当你试图把它挪到另一块 PCB 上时哪些引脚会断、哪些电容会虚焊、哪些走线需要重新布线。关键词 ARM、CMSIS‑4、Cortex‑M、静态工程、源码每一个都不是泛泛而谈的技术标签而是这场尽调行动的坐标原点。2. CMSIS-4 静态工程的本质一套被编译器“信任”的契约2.1 为什么叫“静态工程”它和“动态链接”根本不是一回事很多人看到“静态工程”第一反应是“链接时静态库”这是典型的概念混淆。CMSIS-4 的“静态”指的是其工程结构、接口定义、内存布局、初始化流程全部在编译前就完全确定且不依赖任何运行时动态解析或配置机制。它不是.a文件而是一组被编译器ARMCC、GCC、IAR当作“可信基础设施”直接纳入编译流程的源码集合。我们来看一个最典型的例子system_stm32f10x.c。这个文件里有一段看似普通的代码void SystemInit(void) { /* Reset the RCC clock configuration to the default reset state */ RCC-CR | (uint32_t)0x00000001; RCC-CFGR 0x00000000; RCC-CR (uint32_t)0xFEF6FFFF; RCC-PLLCFGR 0x24003010; RCC-CIER 0x00000000; /* ... */ }这段代码没有函数指针、没有配置表、没有#ifdef宏开关它就是一串硬编码的寄存器写入序列。它的执行时机是Reset_Handler之后、main()之前由启动文件startup_stm32f10x_md.s通过bl SystemInit硬跳转调用。这意味着编译时即确定RCC-CR的地址0x40021000在预处理阶段就被#define RCC_BASE (AHBPERIPH_BASE 0x00001000U)展开最终成为绝对地址常量链接时即固化SystemInit函数的入口地址被写死在向量表__Vectors的第 16 项索引 15该向量表本身是__Vectors符号指向的一段.isr_vector段其起始地址由链接脚本STM32F103CB_FLASH.ld中的PROVIDE(__vector_table_start ORIGIN(FLASH));严格指定运行时不变更一旦烧录这段初始化逻辑就永远固定无法通过外部配置、EEPROM 参数或 OTA 升级来修改其行为除非你重写整个SystemInit并重新编译。这就是“静态”的全部含义它把硬件抽象、时钟树配置、中断向量布局这些本该由操作系统或中间件管理的职责全部下压到编译期完成并以源码形式暴露给开发者审查。它不像 Linux 内核那样有arch/arm/mach-stm32/目录下的设备树解析器也不像 Zephyr RTOS 那样通过dts文件生成generated_dts_board.h。CMSIS-4 的“配置”就是你手动改system_*.c里的数字然后make clean make all。这种设计在资源极度受限64KB Flash、实时性要求严苛10us 中断响应、安全认证等级高IEC 61508 SIL3的场景下是无可替代的优势。但代价是每一次芯片型号变更、每一次时钟方案调整、每一次外设引脚重映射都必须人工审计并修改这一组源码没有任何自动化工具能帮你做“兼容性检查”。我曾见过一个项目因为把system_stm32f4xx.c里的HSE_VALUE从8000000U错写成8000000少了U后缀导致 GCC 在-O2下将HSE_VALUE * 9优化为72000000而实际 HSE 是 25MHz结果 PLL 输出频率偏差 3.2%ADC 采样率全乱。这种错误不会报编译警告只会让硬件测试阶段的工程师抓狂。2.2 CMSIS-4 的核心组件拆解不是“库”是“协议栈”CMSIS-4 的目录结构以官方CMSIS/根目录为例看似简单实则暗藏玄机。它不是按功能分层如 HAL、LL、Driver而是按信任层级和编译依赖关系组织CMSIS/ ├── CoreSupport/ # 最底层编译器无关的内核操作封装__get_PSP, __set_CONTROL等 │ ├── core_cm3.h # Cortex-M3 核心寄存器定义、内联汇编宏、异常处理原型 │ └── core_cm3.c # 极少量 C 实现如 __NVIC_EnableIRQ ├── Device/ # 第二层芯片厂商提供的设备专用层ST、NXP、Silicon Labs等 │ └── ST/ │ └── STM32F10x/ │ ├── Include/ # device.h主头文件、stm32f10x.h外设寄存器定义 │ └── Source/ # startup_*.s启动文件、system_*.c系统初始化 ├── DSP/ # 第三层可选CMSIS-DSP v1.4.0注意CMSIS-4 时代 DSP 是独立子项目 └── RTOS/ # 第四层可选CMSIS-RTOS v1.02POSIX 兼容的 RTOS 封装层关键点在于CoreSupport和Device之间存在严格的单向依赖。device.h必须#include core_cm3.h但core_cm3.h绝对不能#include stm32f10x.h。这种设计确保了内核抽象与芯片实现解耦你可以用同一个core_cm3.h去驱动 STM32、LPC1768、EFM32只要它们都是 Cortex-M3编译器中立性core_cm3.h里所有内联汇编都用__attribute__((always_inline))和#ifdef __GNUC__ / #ifdef __ARMCC_VERSION包裹保证在 ARMCC 5.06、GCC 4.9、IAR EWARM 7.80 下都能正确展开零运行时开销所有__get_MSP()、__enable_irq()都是static inline函数编译后就是 1~2 条汇编指令没有函数调用栈开销。而Device/目录下的内容则是 CMSIS-4 “遗产性”的集中体现。以startup_stm32f10x_md.s为例它定义了完整的向量表__Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler ; ... 共 84 项直到 SysTick_Handler DCD SysTick_Handler DCD 0 ; 0 for reserved vector DCD 0 ; 0 for reserved vector这个向量表的长度84 项、每一项的顺序必须严格按 ARMv7-M 规范、甚至最后一项的0填充都是 CMSIS-4 协议的一部分。如果你删掉SysTick_Handler后面的两个0Keil 会正常编译但某些 Bootloader如 STM32 的 DFU在验证向量表 CRC 时会失败导致无法升级。这不是 bug是协议。CMSIS-4 的本质就是一份用 C 和汇编写成的、被全球嵌入式工具链共同遵守的“硬件-软件握手协议”。它不提供便利它只提供确定性。2.3 CMSIS-4 与 CMSIS-5 的关键分水岭放弃“向前兼容”的代价CMSIS-5 在 2016 年发布时ARM 做了一个重大决策不再保证 CMSIS-4 到 CMSIS-5 的源码级兼容。这不是技术倒退而是架构演进的必然。CMSIS-5 引入了CMSIS-Core(M)、CMSIS-Driver、CMSIS-Pack等新概念其核心变化有三对比维度CMSIS-4CMSIS-5头文件组织core_cm3.hstm32f10x.h单一包含链cmsis_compiler.hcmsis_gcc.hcore_armv7m.h多层抽象启动代码startup_*.s硬编码向量表长度startup_*.csystem_*.clinker_script.ld分离外设访问GPIOA-ODR 0xFF;直接寄存器操作gpio_driver-pinWrite(GPIO_PORT_A, 0, 1);驱动抽象层这个转变的深层原因是CMSIS-4 诞生于“单芯片单固件”时代而 CMSIS-5 面向的是“多核异构、安全隔离、OTA 升级”时代。CMSIS-4 的core_cm3.h里__disable_irq()宏直接展开为cpsid i这是最高效的而 CMSIS-5 的__disable_irq()可能被重定向到 TrustZone 的 Secure Monitor Call以实现安全世界/非安全世界的 IRQ 控制。这种设计差异使得 CMSIS-4 工程迁移至 CMSIS-5 时绝非简单的“替换头文件”就能完成。我参与过一个电力终端项目迁移原 CMSIS-4 工程使用NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)设置优先级分组而 CMSIS-5 的NVIC_SetPriorityGrouping()函数签名已改为void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)且内部实现增加了对SCB-AIRCR寄存器PRIGROUP字段的掩码校验。结果是旧代码传入0x05非法值CMSIS-5 会静默忽略导致所有中断优先级实际为 0系统在高负载下彻底锁死。这种“静默失败”比编译报错更危险。CMSIS-4 的“遗产”价值恰恰在于它的“不进化”——它用最原始的方式把所有不确定性都暴露在源码层面让你一眼就能看清NVIC-IP[0]的每一位代表什么。而 CMSIS-5 的“现代化”则是用一层层抽象去掩盖复杂性代价是调试难度指数级上升。3. 源码级尽调实操从core_cm3.h到startup_stm32f10x_md.s的逐行审计3.1 审计第一步确认 CMSIS-4 版本与芯片手册的精确匹配CMSIS-4 没有统一的版本号它的版本隐含在core_cm3.h文件的注释和宏定义中。不要相信CMSIS/README.txt里的描述要直接看源码。打开CMSIS/CM3/CoreSupport/core_cm3.h查找以下关键标记/** \brief CMSIS Version \details CMSIS Version 4.0.0 */ #define __CM3_CMSIS_VERSION_MAIN (0x04U) /*! [31:16] CMSIS HAL main version */ #define __CM3_CMSIS_VERSION_SUB (0x00U) /*! [15:0] CMSIS HAL sub version */这个__CM3_CMSIS_VERSION_MAIN必须是0x04U且__CM3_CMSIS_VERSION_SUB通常为0x00U或0x01U。但更重要的是要核对core_cm3.h中的内核特性定义是否与你使用的芯片手册一致。例如Cortex-M3 支持MPU内存保护单元但并非所有 M3 芯片都启用它。core_cm3.h里有#if defined (__MPU_PRESENT) (__MPU_PRESENT 1U) #include mpu_armv7.h #endif这里的__MPU_PRESENT不是由 CMSIS 定义的而是由你的启动文件或编译器命令行-D__MPU_PRESENT1传入的。如果芯片手册明确说明该型号如 STM32F103C8不带 MPU但你的工程里却定义了__MPU_PRESENT1那么mpu_armv7.h会被包含其中的MPU-CTRL访问就会触发 HardFault。我在一次尽调中发现某厂商的 SDK 为了“通用性”在所有system_*.c里都加了-D__MPU_PRESENT1结果导致 F103 系列在初始化 MPU 时直接崩溃。解决方法不是删掉定义而是检查mpu_armv7.h中的MPU_Type结构体定义确认其寄存器偏移是否与芯片手册0xE000ED90地址匹配。CMSIS-4 的尽调本质上就是一场“源码-手册-硬件”三方对齐。3.2 审计第二步system_*.c中的时钟树陷阱与数学陷阱system_stm32f10x.c是 CMSIS-4 工程中最容易出错的文件。它的核心任务是计算SystemCoreClock并配置 RCC。我们来看一段典型代码void SystemCoreClockUpdate(void) { uint32_t tmp 0, pllmull 0, pllsource 0, prescaler 0; /* Get SYSCLK source -------------------------------------------------------*/ tmp RCC-CFGR RCC_CFGR_SWS; switch (tmp) { case 0x00: /* HSI used as system clock */ SystemCoreClock HSI_VALUE; break; case 0x04: /* HSE used as system clock */ SystemCoreClock HSE_VALUE; break; case 0x08: /* PLL used as system clock */ /* Get PLL clock source and multiplication factor ----------------------*/ pllmull RCC-CFGR RCC_CFGR_PLLMULL; pllsource RCC-CFGR RCC_CFGR_PLLSRC; if (pllsource 0x00) { /* HSI/2 selected as PLL clock entry */ SystemCoreClock (HSI_VALUE 1) * (pllmull 2); } else { /* HSE selected as PLL clock entry */ SystemCoreClock HSE_VALUE * (pllmull 2); } break; default: SystemCoreClock HSI_VALUE; break; } /* Apply the Prescaler ----------------------------------------*/ prescaler RCC-CFGR RCC_CFGR_HPRE; switch (prescaler) { case 0x00: /* SYSCLK not divided */ break; case 0x08: /* SYSCLK divided by 2 */ SystemCoreClock 1; break; /* ... 其他 case */ } }这段代码的致命陷阱在于pllmull的值是寄存器字段的原始值而非倍频系数。根据 STM32F103 参考手册RCC_CFGR_PLLMULL字段是 4 位其编码为0000- PLL 输入不倍频无效0001- ×20010- ×3...1111- ×16但注意pllmull读出来是0x0000~0x000F而倍频系数是pllmull 2。这个2是硬编码的它假设了 PLL 的最小倍频是 ×2。但如果芯片手册规定某型号的 PLL 最小倍频是 ×3如某些超低功耗型号这个2就错了。我在审计一个 TI MSP432 的 CMSIS-4 移植包时发现其system_msp432p401r.c里pllmull的计算是pllmull 1因为 MSP432 的 PLL 编码规则不同。CMSIS-4 的“标准”在这里失效了——它只定义了接口SystemCoreClockUpdate()函数名没定义实现细节。因此尽调时必须找到你芯片的 Reference Manual定位到 “Clock Control (RCC)” 章节查找 “PLL Configuration Register (RCC_CFGR)” 表格确认PLLMUL字段的编码规则对照system_*.c中的pllmull X计算X 必须与手册一致。这是一个纯手工、不可自动化的步骤。任何自动化脚本都无法替代对芯片手册的逐字阅读。3.3 审计第三步startup_*.s向量表的物理地址与链接脚本对齐startup_stm32f10x_md.s的向量表其物理地址必须与链接脚本.ld文件中__vector_table_start的定义完全一致。这是 CMSIS-4 静态工程最脆弱的环节。我们来看一个典型链接脚本片段MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . ALIGN(4); } FLASH }这里ORIGIN 0x08000000是 Flash 起始地址.isr_vector段被放置在 Flash 的最开头。但startup_*.s里向量表的起始地址是由汇编器的ENTRY(Reset_Handler)和__Vectors符号决定的。如果链接脚本里 FLASH写成了 RAM或者.isr_vector段被其他段如.text意外覆盖后果是灾难性的MCU 上电后从0x20000000RAM 起始取第一条指令而那里是随机数据直接 HardFault。尽调时必须用arm-none-eabi-objdump -d your.elf反汇编找到.isr_vector段的地址并与your.map文件中的__Vectors符号地址对比。例如Disassembly of section .isr_vector: 08000000 __Vectors: 8000000: 20005000 andcs r5, r0, r0 8000004: 08000145 stmdaeq r0, {r0, r2, r6, r7, r8, r9, sl, fp, sp, lr} ...同时在your.map中查找0x08000000 __Vectors . 0x08000000 . ALIGN (0x4) 0x08000000 *(.isr_vector)两者地址必须完全相等0x08000000。如果不等说明链接脚本或启动文件有误。我曾遇到一个案例某 SDK 的startup_*.s里__Vectors符号被定义在.data段而不是.isr_vector段导致向量表被放在 RAM 里而 Bootloader 只校验 Flash 中的向量表 CRC结果固件永远无法通过安全启动。CMSIS-4 的“静态”意味着每一个地址、每一个偏移、每一个符号都必须是确定的、可验证的、可追溯的。3.4 审计第四步外设头文件中的位域陷阱与内存映射一致性stm32f10x.h这类外设头文件是 CMSIS-4 工程中“最危险”的部分。它用 C 结构体模拟寄存器但 C 标准对位域bit-field的内存布局没有强制规定不同编译器ARMCC vs GCC可能生成不同的内存布局。例如typedef struct { __IO uint32_t CR1; /*! USART Control register 1, Address offset: 0x00 */ __IO uint32_t CR2; /*! USART Control register 2, Address offset: 0x04 */ __IO uint32_t CR3; /*! USART Control register 3, Address offset: 0x08 */ __IO uint32_t BRR; /*! USART Baud rate register, Address offset: 0x0C */ __IO uint32_t GTPR; /*! USART Guard time and prescaler register, Address offset: 0x10 */ __IO uint32_t RTOR; /*! USART Receiver Time Out register, Address offset: 0x14 */ __IO uint32_t RQR; /*! USART Request register, Address offset: 0x18 */ __IO uint32_t ISR; /*! USART Interrupt and status register, Address offset: 0x1C */ __IO uint32_t ICR; /*! USART Interrupt flag Clear register, Address offset: 0x20 */ __IO uint32_t RDR; /*! USART Receive Data register, Address offset: 0x24 */ __IO uint32_t TDR; /*! USART Transmit Data register, Address offset: 0x28 */ } USART_TypeDef;这个结构体本身没问题但当你访问USART1-CR1的某一位时比如USART1-CR1 | USART_CR1_UE;USART_CR1_UE定义为0x00002000U这是安全的。但如果你用位域typedef struct { uint32_t UE:1; /*! USART Enable */ uint32_t UESM:1; /*! USART Enable in Stop Mode */ // ... 其他位 } USART_CR1_Bits;那么USART_CR1_Bits *cr1 (USART_CR1_Bits*)USART1-CR1; cr1-UE 1;的行为在 ARMCC 下可能是正确的但在 GCC 下可能因为位域打包顺序不同而写错寄存器。CMSIS-4 的官方头文件从来不用位域它只用宏定义#define USART_CR1_UE ((uint32_t)0x00002000U)和整数运算。尽调时必须检查你工程中所有自定义的外设访问代码禁止使用位域操作寄存器。此外还要核对stm32f10x.h中的基地址定义#define USART1_BASE (APB2PERIPH_BASE 0x00004000U) #define APB2PERIPH_BASE (PERIPH_BASE 0x00010000U) #define PERIPH_BASE (0x40000000U)这个0x40000000U必须与芯片手册中 “Memory Map” 章节的 “Peripheral memory map” 表格完全一致。如果手册写的是0x40000000而头文件写成了0x40000000UL长整型在某些极老的编译器下可能导致类型转换警告进而影响优化。CMSIS-4 的尽调就是要把这些“看起来理所当然”的数字全部拉回芯片手册的原始出处进行逐字比对。4. 迁移约束分析从 CMSIS-4 到新平台的七道关卡4.1 关卡一编译器 ABI 兼容性——ARMCC 5.06 的“古董级”约定CMSIS-4 工程绝大多数诞生于 ARM Compiler 5ARMCC 5.06时代。ARMCC 5.06 使用的是 AAPCSARM Architecture Procedure Call Standard的早期变种其 ABIApplication Binary Interface与现代 GCCARM GCC 10或 ARM Compiler 6ARMCLANG存在细微但致命的差异。最典型的是浮点参数传递规则。ARMCC 5.06 规定float和double参数必须通过浮点寄存器s0-s15传递而 AAPCS-ABIGCC 采用规定float通过s0-s15double通过d0-d7即s0/s1,s2/s3成对使用。如果你的 CMSIS-4 工程里有一个函数void adc_calibrate(float ref_voltage, double gain_factor);在 ARMCC 5.06 下ref_voltage在s0gain_factor在d0即s0/s1而在 GCC 下ref_voltage在s0gain_factor在d0也是s0/s1看起来一样。但问题出在函数调用约定的堆栈对齐上。ARMCC 5.06 要求堆栈在函数入口处 8 字节对齐而 AAPCS-ABI 要求 8 字节对齐但 GCC 在-mfloat-abihard下会插入额外的push {r4-r7,lr}指令改变堆栈帧布局。结果是CMSIS-4 的startup_*.s里bl main调用后GCC 编译的main()函数看到的堆栈指针SP位置与 ARMCC 预期不同导致局部变量访问错位。解决方案不是改代码而是在 GCC 编译时强制使用 ARMCC 5.06 的 ABI-mabiaapcs -mfloat-abihard -mfpuvfp并禁用 GCC 的堆栈保护-fno-stack-protector。但这只是权宜之计真正的迁移必须重写所有涉及浮点运算的函数用整数运算替代或使用 CMSIS-DSP 的固定点函数。4.2 关卡二中断向量表重定位——Bootloader 与 Application 的主权之争CMSIS-4 工程的向量表默认位于0x08000000Flash 起始。但现代嵌入式系统普遍采用双 Bank OTA 或 Bootloader Application 架构。此时Application 的向量表必须重定位到新的地址如0x08008000。CMSIS-4 的startup_*.s里向量表地址是硬编码的AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler ; ...要重定位传统做法是修改链接脚本让.isr_vector段从0x08008000开始。但这要求 Bootloader 在跳转前必须执行SCB-VTOR 0x08008000;。而 CMSIS-4 的core_cm3.h里SCB-VTOR的定义是#define SCB_VTOR_TBLOFF_Msk (0xFFFFFFUL SCB_VTOR_TBLOFF_Pos) /*! SCB VTOR TBLOFF Position */ #define SCB_VTOR_TBLOFF_Pos (7UL) /*! SCB VTOR TBLOFF Position */注意0xFFFFFFUL—— 这个掩码假设TBLOFF字段是 24 位最大偏移0xFFFFFF * 4 0x3FFFFFC字节。但如果你的 Application 起始地址是0x08008000TBLOFF (0x08008000 - 0x08000000) / 4 0x2000这完全在范围内。问题在于CMSIS-4 的SystemInit()函数里没有SCB-VTOR的设置代码。它假设向量表永远在0x08000000。因此迁移时你必须在main()的最开头手动添加// 在 main() 第一行 SCB-VTOR 0x08008000UL; // 或者更安全SCB-VTOR (uint32_t)__Vectors; __DSB(); __ISB();并且__Vectors符号必须在链接脚本中正确定义。这打破了 CMSIS-4 “开箱即用”的承诺变成了一个必须手动干预的迁移点。很多 Bootloader 文档会告诉你“设置 VTOR 即可”但没告诉你 CMSIS-4 的SystemInit()会悄悄把 VTOR 改回去如果它检测到 PLL 未锁定会执行SCB-AIRCR ...而AIRCR的写入会重置 VTOR。所以尽调时必须搜索整个 CMSIS-4 源码确认没有任何地方会修改SCB-VTOR。4.3 关卡三启动代码的汇编语法鸿沟——ARMASM 与 GNU AS 的战争CMSIS-4 的startup_*.s是用 ARMASMARMCC 的汇编器语法写的。迁移到 GCC 时必须将其转换为 GNU AS 语法。这不是简单的.s改.S而是语法层面的重构。例如ARMASM 的