ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Keil MDK手写STM32汇编启动代码:从复位向量到点灯全流程

Keil MDK手写STM32汇编启动代码:从复位向量到点灯全流程 用标准库或者HAL库在Keil里点灯教程一抓一大把。但要说在Keil下亲手写完整一段汇编程序让芯片上电后从第一条指令开始完全由你说了算很多人心里反而发怵。我身边不少工程师朋友写了好几年STM32对HAL库的API如数家珍却从没完整读过一遍默认生成的Startup.s——而这个文件恰恰是整个工程真正的入口。标题里这个需求说白了就是要把这个黑盒彻底打开。这篇文章我拿STM32F103C8T6当靶子来走完整条链路因为这个芯片在Keil里建工程最省事资料也多。内容会覆盖四个层面Keil的编译链是怎么分工的、一个纯汇编最小工程怎么搭、汇编怎么直接操作寄存器点灯、以及汇编和C混编时最容易被坑的地方。不管你是刚接触嵌入式的小白还是已经在用HAL库写业务的老手跟着把流程走一遍之后再回去看默认启动文件你会觉得它瞬间亲切了很多。1. 认清Keil的编译链汇编不是备选项而是地基1.1 C编译器、汇编器、链接器各管哪一段很多人对Keil的认知停留在写C代码、编译、烧录但一次完整的构建过程远比这个链子长。在Keil MDK里你写的.c文件由C编译器处理新版是Arm Compiler 6历史版本叫ARMCC它负责把C代码翻译成目标文件.o。而你写的.s汇编文件则由汇编器armasm处理成.o。最后链接器armlink会把所有目标文件和库文件按照分散加载文件制定的地址规则布局到存储器的指定位置输出一个AXF可执行文件。这个AXF是Keil调试时的核心文件里面同时包含了机器码和调试符号信息烧录时再从AXF里提取HEX或BIN数据。为什么我上来就要拆开讲这个流程因为当你新建一个.s文件、写下一段汇编时你是在和汇编器直接对话而不是和C编译器对话。汇编层报错十有八九是语法、标号、段定义的问题和你的C代码毫无关系。搞清楚这个边界编译报错时就不会满工程瞎找。尤其是看到Undefined symbolArea directive missing这类提示第一反应应该是去检查汇编源文件而不是打开main.c。另外一个容易忽略的点是C代码的性能上限其实在编译器生成汇编的那一刻就决定了。你在C里写一个循环编译器可能会做各种优化也可能生成你完全看不懂的指令序列。而当你需要精确控制时序、需要计算每条指令耗了多少个时钟周期时只有汇编能给出确定性的答案。这个能力在写启动代码、临界区保护、精确延时、低功耗唤醒这些场景里是不可替代的。1.2 Keil MDK和Keil C51汇编语法是两码事这里必须先踩一个几乎所有初学者都会踩的坑Keil家族里有两个常用产品线一个是针对ARM Cortex-M系列的Keil MDK另一个是针对8051系列单片机的Keil C51。两者虽然都叫Keil但汇编指令集完全是两套体系。MDK下面写的是ARM汇编采用UAL统一汇编语言配合Thumb/Thumb-2指令集寄存器是R0到R15、SP、LR、PC寻址风格是LDR/STR而C51下面写的是8051汇编寄存器是A、B、DPTR指令是MOV、MOVX、CJNE这类。维度Keil MDKKeil C51目标芯片Cortex-M0/M3/M4/M7 等8051 及增强型代表汇编器armasmA51寄存器R0-R15、SP、LR、PC、xPSRA、B、DPTR、PSW典型指令LDR、STR、BL、BX、CPSMOV、MOVX、CJNE、DJNZ汇编文件后缀.s.a51或.asm如果你在学校学的是《单片机原理》里的8051汇编那套经验在STM32的Keil工程里第一行就编译不过。反过来也一样把ARM汇编的LDR/STR套到8051工程里同样是寸步难行。所以动手之前先明确目标芯片是哪颗这个判断决定了后面所有语法细节。我下面的内容默认前提是Keil MDK加STM32F103C8T6也就是Cortex-M3内核如果你手头是8051指令虽然不同但工程搭建思路完全相通。2. 第一步不是写代码而是把工程骨架搭对2.1 新建工程时反复权衡要不要勾启动文件在Keil uVision5里新建工程路径是Project - New uVision Project然后选择芯片型号。这一步有个前提容易被忽略对应的Device Pack得先装好。STM32F103C8T6对应的包是Keil.STM32F1xx_DFP可以在Pack Installer界面勾选下载也可以从官网下载离线包手动双击安装。如果在线下载一直报硬件错误或者断断续续我的做法是直接放弃在线方式去官网把离线Pack下载下来本地双击一下几秒钟就装好了过程丝滑得多。选完芯片之后Keil会弹出Manage Run-Time Environment窗口里面默认帮你勾了一堆组件其中就有Device下的Startup。这个组件本质上就是编译好的启动文件模板包含向量表和复位代码。如果我们要写一个自己的汇编程序最干净的做法是在这个窗口里把Startup的勾取消然后自己添加一个.s文件。这样既不会被默认启动文件干扰又能通过自己的汇编代码掌控整个芯片上电后的第一条指令。当然如果你是第一次练习也可以保留默认启动文件只在main.c里添加一个汇编函数来调用这是混编的玩法后面第四章会细讲。但如果目标是完成一个汇编程序的编写我强烈建议至少有一次是纯汇编启动。只有亲手写过复位入口你才会真正明白程序的入口不是main这句话是什么意思。2.2 手写最小启动代码复位向量和中断向量表在Cortex-M系列芯片里任何工程都要有一个启动代码它的核心职责有两个定义中断向量表、给出复位入口。中断向量表本质上是一块内存区域里面按固定顺序存放各个异常和中断处理函数的地址。芯片上电时硬件会从向量表的第0项取出栈顶地址从第1项取出复位异常入口然后跳转执行。这就是嵌入式世界里最底层的开机引导。一个最小可运行的启动代码长这样; 最小启动代码针对 STM32F103C8T6 PRESERVE8 THUMB AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0x20005000 ; 栈顶地址指向 RAM 顶端 DCD Reset_Handler ; 复位异常入口 AREA |.text|, CODE, READONLY Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main LDR R0, __main BX R0 ENDP END这里要区分一个概念AREA、DCD、EXPORT、IMPORT这些不是CPU指令而是汇编器伪指令。它们是给汇编器看的不直接生成机器码但会决定段的位置、符号的可见性和链接行为。逐个解释一遍PRESERVE8要求代码保持8字节对齐这是AAPCS调用约定的一部分混编时必须遵守。THUMB声明当前代码是Thumb指令集。Cortex-M3只支持Thumb/Thumb-2不支持老的ARM模式。AREA RESET, DATA, READONLY定义一个只读数据段专门放向量表。AREA |.text|, CODE, READONLY定义代码段。注意.text两边的竖线不能省这是armasm里对带特殊字符段名的写法。DCD是按32位分配一个存储单元并填入初始值。这里栈顶地址写0x20005000对应STM32F103C8T6的20KB SRAM顶端因为栈是向下增长的所以把栈顶初始化到RAM区域的最高地址。EXPORT导出符号让链接器在其他文件里也能引用。[WEAK]表示弱定义如果用户在其他地方重新定义了同名符号链接器会优先使用用户版本。IMPORT __main声明引用C库入口。__main会完成RW段拷贝、ZI段清零、堆栈库初始化最后再调用C的main。LDR R0, __main; BX R0是先加载入口地址到R0再跳过去执行。如果你要写纯汇编工程、不想链接C库也可以把入口换成自己的函数但必须手动初始化栈指针Reset_Handler PROC EXPORT Reset_Handler [WEAK] LDR SP, 0x20005000 BL my_main Loop B Loop ENDP注意这里我显式把栈指针设置好因为不经过__main就没有栈初始化过程。很多人在这里翻车代码跳进去了栈指针却是0一调用子程序就直接进HardFault查半天都找不到原因。2.3 工程配置里三个必改项写好.s文件后工程配置还有三个关键点必须检查任何一个没设对后面都会出幺蛾子。第一Output选项卡里勾选Create HEX File。否则编译完成后只生成AXF拿不到烧录文件。第二Target选项卡里填写Flash和RAM的起始地址与大小。STM32F103C8T6的Flash是64KB起始地址0x08000000RAM是20KB起始地址0x20000000。这个地址信息会作为链接器默认分散加载的根地址不填对代码就不知道该放哪里。第三Debug选项卡里选择调试方式。手头没有开发板就先选Simulator用软件模拟运行用ST-Link就选ST-Link Debugger并且在Flash Download里添加对应的烧录算法。这三个开关里最容易忽略的是Flash Download里的烧录算法。很多人能编译能进入调试界面但一点下载就报Cannot load flash programming algorithm或者某个地址写入失败多半就是这里没选对算法。要选和芯片Flash容量匹配的算法比如STM32F103系列通常选STM32F10x High-density Flash 512K。3. 用寄存器寻址点一颗LED芯片到底在做什么3.1 点灯这件事底层只有几次寄存器操作我们先忘掉HAL库想想HAL_GPIO_WritePin到底做了什么。它的本质就是在指定的内存地址里写入一个数字。STM32的寄存器映射就是芯片厂商把外设的控制寄存器编址到一段段内存空间里CPU访问外设寄存器和访问普通内存的指令没有任何区别都是LDR加载和STR存储。这也是为什么汇编能如此贴近底层——你不需要任何库只需要知道地址和位字段然后用几条指令把值写进去。点亮一颗LED在STM32F103上最少需要这几步使能GPIOB外设时钟。GPIOB是挂在APB2总线上的外设不打开时钟后续对GPIOB寄存器的操作全部无效。RCC基地址是0x40021000APB2ENR偏移是0x18GPIOB的使能位是bit3。配置PB0为推挽输出。GPIOB基地址是0x40010C00CRL寄存器偏移是0x00PB0对应低4位其中MODEx[1:0]设置为输出模式CNFx[1:0]设置为推挽。往ODR寄存器写0或1。ODR偏移是0x0C第0位对应PB0。延时翻转电平循环。HAL库帮你干了所有这些活但最终CPU执行的指令本质上就是LDR、ORR、BIC、STR这几条。很多人问为什么要学汇编这就是最有说服力的应用场景你要精确控制某个引脚时序时编译器优化出来的C代码未必符合你的预期而汇编编写的寄存器操作是确定的。3.2 点灯汇编代码逐行拆解下面是一份可以直接编译运行的STM32F103C8T6汇编点灯程序以PB0点灯为例。时钟方面我直接用芯片复位后的默认HSI内部时钟不配置PLL这样省去时钟初始化的复杂度专注看寄存器操作。; ------------------------------------------------------------ ; LED Blink - STM32F103C8T6 ; 硬件连接: PB0 接 LED 正极负极经限流电阻到 GND ; ------------------------------------------------------------ PRESERVE8 THUMB AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0x20005000 DCD Reset_Handler AREA |.text|, CODE, READONLY ; 寄存器基址 RCC_BASE EQU 0x40021000 GPIOB_BASE EQU 0x40010C00 RCC_APB2ENR EQU 0x18 GPIOB_CRL EQU 0x00 GPIOB_ODR EQU 0x0C LED_PIN EQU 0x00000001 Reset_Handler PROC EXPORT Reset_Handler [WEAK] ; 1. 打开 GPIOB 时钟 LDR R0, RCC_BASE LDR R1, [R0, #RCC_APB2ENR] ORR R1, R1, #(1 3) ; bit3 IOPBEN STR R1, [R0, #RCC_APB2ENR] ; 2. 配置 PB0 为推挽输出最大速度 50MHz LDR R0, GPIOB_BASE LDR R1, [R0, #GPIOB_CRL] BIC R1, R1, #0x0F ; 先清低4位即清除CNF0和MODE0 ORR R1, R1, #0x03 ; MODE011 - 50MHz 输出 STR R1, [R0, #GPIOB_CRL] blink_loop ; 3. 点亮 LEDODR 第0位置 1 LDR R1, [R0, #GPIOB_ODR] ORR R1, R1, #LED_PIN STR R1, [R0, #GPIOB_ODR] BL delay ; 4. 熄灭 LEDODR 第0位清 0 LDR R1, [R0, #GPIOB_ODR] BIC R1, R1, #LED_PIN STR R1, [R0, #GPIOB_ODR] BL delay B blink_loop ; 软件延时子程序 delay PROC LDR R2, 0x3FFFFF delay_loop SUBS R2, R2, #1 BNE delay_loop BX LR ENDP END这份代码有四个信息量很大的点值得单独拿出来讲。第一个是LDR R0, RCC_BASE这种写法。它其实是伪指令不是真正的LDR指令。汇编器看到LDR Rx, 常数会把这个32位常数放到代码段旁边的文字池里然后生成一条PC相对加载指令从文字池里把数值取到寄存器。好处是常数不受立即数编码位数限制坏处是多占几个字节的代码空间。所以如果常数很小比如配置CRL时的#0x03直接写成立即数就行一条指令搞定。第二个是ORR R1, R1, #(1 3)。在armasm里这个移位写法是合法的汇编器会把它当成一个常数值0x08来处理。如果汇编器提示立即数越界可以把1 3改写成0x08或者拆成多条MOV/MOVW/MOVT来实现。第三个是BIC R1, R1, #0x0F。BIC是位清除指令对应C语言里的R1 ~0x0F。配置寄存器时先清后置是固定套路防止上一次配置的残留位影响本次操作。第四个是BL delay和BX LR的配合。BL是带返回的跳转跳转前硬件会把下一条指令地址自动保存到LR寄存器所以子程序末尾BX LR能准确回到调用处。这就是函数调用的最底层实现。3.3 打开反汇编窗口看看汇编器把指令变成了什么写完后编译、进入Debug模式再打开View - Disassembly Window你会看到一件有意思的事LDR R0, RCC_BASE在反汇编窗口里很可能变成LDR R0, [PC, #0x18]这样的形式。这说明真正的机器指令并不是直接加载常量而是从文字池中读取。PC寄存器在Thumb模式下是当前指令地址加4汇编器会自动计算这个偏移量。这个细节看起来琐碎但理解了它你就明白了两个重要结论第一LDR R0, 0x12345678会额外消耗4字节写高频代码时如果立即数能放进MOVS的范围尽量用MOVS第二文字池紧跟在当前代码区域如果你在代码段里写了很大的数据块PC相对偏移可能会超过范围汇编器有时会报out of range literal之类的错误这个时候可以手动在代码段里插入LTORG指令强制把文字池分散放置。看完反汇编窗口之后你对编译器到底把我的代码变成了什么这个问题会有非常直观的感知。这也是我后来调试诡异bug时的常用手段先看反汇编再判断问题出在逻辑层还是优化层。4. 汇编和C混编实际项目最常用的配合方式4.1 启动文件用汇编业务逻辑用C在实际项目里没人会用汇编写完整套业务逻辑除非你在做那种极限性能的算法库或者加密内核。大多数情况下的合理分工是启动代码、异常处理、精确延时、临界区保护这类对时序和现场要求极高的部分用汇编协议栈、UI、业务控制逻辑用C。Keil默认工程的启动文件之所以是汇编原因就在这里中断向量表地址必须绝对准确中断发生时的压栈行为由ARM架构定死复位后的第一条指令要设置栈指针这些对时间和顺序极端敏感的操作用汇编表达最清晰也最不容易被编译器优化干扰。这个分工在工程层面还有一个妙处汇编负责给C搭好一个可运行的平台C负责在平台上做复杂逻辑。比如复位入口先设置SP、再初始化时钟、然后跳转__main完成C运行时环境搭建最后进入main函数让业务逻辑接管。C代码看到的世界是一个干净、可预期、有栈可用的世界这就是启动文件的核心价值。4.2 AAPCS汇编和C之间传参的约定要让汇编和C顺畅互调必须先理解AAPCS也就是ARM应用程序过程调用标准。它的核心规则我用三句话概括前4个整数参数依次放在R0到R3第5个参数开始压栈。返回值放在R064位返回值用R0和R1组合。被调用函数可以修改R0-R3但如果使用R4-R11必须先保存再恢复。举个最实用的例子。假设C文件里声明了一个汇编延时函数// main.c #include stdint.h extern void delay_asm(uint32_t count); int main(void) { while (1) { delay_asm(0xFFFFF); } }汇编文件这样写AREA delay_code, CODE, READONLY EXPORT delay_asm delay_asm SUBS R0, R0, #1 BNE delay_asm BX LR END调用时参数0xFFFFF被调用方放在R0里子程序通过R0自减实现循环递减递减到0时Z标志位置位BNE不再跳转最后BX LR返回主程序。整个子程序只用了R0完全符合AAPCS不需要额外保存寄存器非常干净。反方向也一样。在汇编里调用C函数IMPORT my_c_function LDR R0, 0x3F LDR R1, 0x1234 BL my_c_function调用前把参数放进R0、R1BL跳过去函数返回后结果在R0里。这里有一个很容易踩的坑BL会自动把下一条指令地址写入LR如果你在BL调完一个函数后还想继续调另一个函数最好提前把LR压栈保存否则第二次BL会覆盖第一次的返回地址最终函数返回时就飞了。4.3 EXPORT、IMPORT和符号可见性在汇编和C混编时符号规则必须搞明白。EXPORT相当于把当前符号设为全局可见类似C语言里的非static函数。IMPORT则是对外部符号的声明类似C里的extern。如果汇编文件里要调用一个C函数就需要先IMPORT它如果C文件要调用汇编函数汇编文件里就要EXPORT它。[WEAK]修饰符表示弱引用或弱定义。当链接器找不到符号时不会直接报错而是使用默认值或0。启动文件里EXPORT Reset_Handler [WEAK]的实际意义是如果你在工程其他地方实现了Reset_Handler链接器会采用你的版本如果没有就用启动文件里这个默认版本。这个机制让用户既可以使用默认启动逻辑又能在需要时无缝覆盖。还有一个细节ARMCC和armclang编译C文件时函数符号名不会自动加下划线前缀。也就是说C函数delay_asm在汇编文件里就是IMPORT delay_asm不需要额外处理。但如果你用了GNU风格的交叉工具链某些平台会带下划线前缀这个需要根据具体工具链调整。Keil MDK默认不带写错最多就是链接时报undefined symbol根据报错名字也能很快定位。4.4 混编时最容易踩的坑入口不是main混合工程里如果自己手写启动代码最容易出错的地方不是语法而是跳错了运行环境。一些人图省事直接在Reset_Handler里跳转到C的main函数结果发现全局变量值是错的或者静态变量没有被正确初始化。原因很简单C程序的运行环境不是天然就绪的。在进入main之前C库的__main会执行一系列初始化把RW段从Flash拷贝到RAM、把ZI段清零、建立堆栈和堆的运行环境最后才调用你的代码。直接从复位入口跳main等于跳过了所有这些准备工作。所以标准做法是启动代码里IMPORT __main再跳转过去让__main完成初始化后进入main。网上有些精简例程直接跳main还能跑通常是因为那个工程里没有全局变量、没有依赖静态初始化、也不使用malloc属于碰巧能跑。正式项目里不要这么干一旦代码规模变大各种诡异问题都会冒出来。5. 调试验收阶段必踩的坑和排查链路5.1 用Simulator先跑通逻辑写汇编最痛苦的地方不是编译不过而是编译过了但板上不亮。这时候如果每次都接开发板烧录效率太低我一般会先在Simulator里把逻辑跑通。在Debug选项卡里把调试器选成Simulator进入调试界面后每次单步执行都能看到R0-R15、SP、LR、PC这些寄存器的实时变化。比如执行点灯代码时在Memory窗口输入0x40010C0C能看到ODR寄存器里的值随着ORR/STR和BIC/STR在0和1之间跳动。这种反馈速度比拿万用表去捅引脚快太多了。Simulator对验证栈指针、LR跳转这类底层逻辑也特别有用。单步进入BL delay时可以看到LR被写入返回地址执行到BX LR时PC又跳回调用处。这些过程如果在纸上画半天都理不清在调试器里单步两次瞬间就明白了。5.2 硬件调试最常见的五类报错不管是用ST-Link还是其他调试器硬件调试阶段的报错基本都逃不出下面这几类现象可能原因排查建议No ST-LINK detected / No ULINK Device found调试器没被识别或接线不对换USB口、重装驱动、确认目标板供电在Options-Debug里选对调试器型号Cannot load flash programming algorithmFlash Download里没添加烧录算法在Debug-Settings-Flash Download里添加与芯片匹配的算法Missing .axf file / 找不到输出文件编译失败或路径变动看Build Output窗口定位编译错误Clean Target后再RebuildError R6002编译运行环境相关多见于系统环境异常检查Keil安装路径权限、关闭杀软干扰、必要时重装或换版本Pack Installer 下载硬件错误在线下载Pack不完整或网络不稳去官网下载离线Pack安装包双击本地安装单独说说Error R6002这类报错。严格来说它不是Keil工程本身的问题而是编译工具运行环境的错误经常出现在老工程迁移到新版本、系统里又装了多个编译器版本导致环境变量混乱的场景。我的排查顺序是先确认Keil版本和补丁号再看安装目录是否有写权限最后关掉杀毒软件对Keil安装目录的实时防护再试一次。大多数时候这类问题是环境问题不是代码问题折腾代码是没用的。另外提一句国产芯片。现在用GD32、CKS32这类替代芯片的人越来越多它们在Keil里的流程和STM32基本一致安装对应的Device Pack之后就像STM32一样选择型号即可。烧录时GD-Link通常也走SWD协议在Debug里选择CMSIS-DAP或厂商专用选项都能用关键是驱动要装好否则同样会报设备找不到。5.3 工程文件夹改名引发的连锁问题有人问keil外面的文件夹名称更改后出了很多问题这事儿我深有体会。Keil工程文件内部记录了大量路径信息包括源文件引用、Include路径、分散加载文件路径、烧录算法路径等等。删除或重命名外层文件夹后原工程里的相对路径大部分还能对上但只要你曾经添加过绝对路径形式的用户文件改名后就会报错。更隐蔽的是工程文件里记录的某些用户配置可能还指向旧目录导致编译输出和调试下载双双异常。处理方式不是重新拖文件而是打开Options for Target逐个检查C/C选项卡里的Include Paths、Linker选项卡里的Scatter File、Debug选项卡里的Flash Download路径。如果改动太大我的经验是把工程文件夹整个复制一份到新目录在新目录下打开工程让它重新生成一次相对路径然后再把旧文件夹删掉。这个方法能省掉手动改几十处路径的时间。6. 让汇编代码更好维护的几个实战习惯汇编代码写起来爽维护起来如果不注意三个月后回头看就是天书。我整理了几条自己常用的习惯分享出来供参考。第一给寄存器基址和位字段起名字。用EQU定义有意义的常量不要在一堆LDR R0, 0x40021000里裸写魔法数字。我看到过有人写汇编全程用裸地址读起来像在解谜调试时根本分不清哪个地址管哪个外设。命名之后代码可读性提升一个档次。第二注释里写清楚为什么而不是照抄指令。ORR R1, R1, #0x08下面写一句使能GPIOB时钟这是有价值的如果能再补一句因为GPIOB挂在APB2上对应APB2ENR的bit3那就更有价值了。查数据手册的记忆会随着项目结束而消失但注释可以帮你找回上下文。第三能拆子程序就拆子程序。复用频率高的延时、关键寄存器设置、外设初始化写成可调用的子程序用清晰的标号分隔不要在一个无限循环里把所有逻辑堆完。汇编虽然没有函数的类型安全但通过AAPCS约定传参一样可以写出模块化风格。第四用宏封装重复片段。armasm支持MACRO/MEND可以把读寄存器→置位→写寄存器这类高频操作定义成宏后续调用直接一行解决。这样写出来的代码简洁也不容易在细节上出错。最后一件事给新人一个建议不要背指令表。汇编指令不需要记全记住LDR/STR加载存储、B/BL/BX跳转、MOV/MVN数据传输、ADD/SUB/AND/ORR/BIC运算、CMP/SUBS条件判断、PUSH/POP栈操作这十来个就能应付九成以上的工程场景。遇到不认识的指令现查ARM Architecture Reference Manual反而比死记硬背更高效。把完整流程走下来你会发现汇编没那么高不可攀它不过是把芯片从复位到引脚电平变化这件事用芯片听得懂的语言重新讲了一遍。Keil给的启动文件、分散加载文件过去被你当成黑盒的那些东西本质上也都是这个套路。哪天你手头项目遇到奇怪的HardFault或者想搞明白为什么某段代码必须在RAM里运行回来翻一遍汇编答案往往比你想的更简单。我自己现在还会时不时光顾反汇编窗口不为别的就是想看看这一版编译器优化到底把我写的高层逻辑变成了什么。这个习惯比背十遍指令表都有用。
返回列表