我们在写单片机裸机程序时,在主函数之前,会有一段启动代码,而启动代码是用汇编写的,有些朋友可能看到汇编头都大了,当时要想深入研究底层架构,这快硬骨头就必须去啃。 汇编:汇编文件转换为目标文件(里面是机器码)。 反汇编:可执行文件(目标文件,里面是机器码),转换为汇编文件。 关于汇编的基础知识,请看笔者以前的文章。 今天笔者以STM32F1的点灯程序为例,带领大家进行反汇编,并阅读反汇编后的代码。1新建LED裸机程序 关于STM32裸机程序的创建,请看笔者博文: https:bruceou。blog。csdn。netarticledetails78244735 但是今天这个程序非常简单,不应那么复杂。 1。新建文件夹 新建文件夹STM32F1,当然名字也可以另取,在STM32F1文件夹下,我们新建五个文件夹,分别为CMSIS、Listing、Output、Project、User。 其中CMSIS文件夹放启动文件: 笔者的开发板芯片是STM32F103ZE,这个文件是根据STM32的固件库startupstm32f10xmd。s文件修改而来。 2。新建工程 打开Keil,在工具栏ProjectNewVisionProject新建我们的工程文件。 输入工程名,保存即可。 窗口是让我们选择公司跟芯片的型号,我们用的芯片是ST公司的STM32F103ZE,有64KSRAM,512KFlash,属于高集成度的芯片。按如下选择即可。 然后点击项目管理。 最后修改后的内容如下: 并添加相应的文件。 其中main。c的内容如下所示:brief延时函数paramdretvalNonevoiddelay(intd){while(d);}briefmainparamNoneretvalintintmain(void){unsignedintpR使能GPIOBpReg(unsignedint)(0x400210000x18);pReg(13);设置GPIOB0为输出引脚pReg(unsignedint)(0x40010C000x00);pReg(10);pReg(unsignedint)(0x40010C000x0C);while(1){设置GPIOB0输出1pReg(10);delay(1000000);设置GPIOB0输出0pReg(10);delay(1000000);}} startup。s文件的内容如下:;STM32F1;FileName:startup。s;Author:BruceOu;Version:V1。0;Date:20210627;Description:STM32F10xMediumDensityDevicesvectortableforMDKARM;toolchain。;Thismoduleperforms:;SettheinitialSP;SettheinitialPCResetHSetthevectortableentrieswiththeexceptionsISRCBranchestomainintheClibrary(callsmain())。;AfterResettheCortexM3processorisinThreadmode,;priorityisPrivileged,andtheStackissettoMain。;PRESERVE8THUMB;VectorTableMappedtoAddress0atResetAREARESET,DATA,READONLYEXPORTVectorsVectorsDCD0DCDResetHResetHandlerAREA。text,CODE,READONLY;ResethandlerResetHandlerPROCEXPORTResetHandler〔WEAK〕IMPORTmainLDRSP,(0x200000000x10000)BLmainENDPEND 接下来还有配置工程。 选择Output文件夹。 选择Listing文件夹。 基本配置就这些,接下来编译工程。 只要没有错误就可以了,最后就是下载程序,笔者使用的是JLink,最后的现象如下: LED会不停闪烁。2Keil反汇编 接下来才是今天正题,反汇编。 在KEIL的User选项中,如下图添加这两项:fromelfbinoutputSTM32F1。bin。。OutputSTM32F1。axffromelftextacoutputSTM32F1。dis。。OutputSTM32F1。axf 然后重新编译,即可得到二进制文件STM32F1。bin(以后会分析)、反汇编文件STM32F1。dis。 如下图所示: 正常编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。 但是反编译是讲为二进制文件反编译成汇编文件,因此反汇编的流程如下: 3反汇编代码解析 接下来就是查看反编译代码,打开反编译文件ProjectSTM32F1。dis。这里只截取一段查看,因为格式都是一样的,知识每条内容不同罢了。 第一列是链接地址,第二列是机器码,第三列是汇编指令。 根本汇编指令,我们找到ARMv7MArchitectureReferenceManualDDI0403E。d(ID070218)中的LDR指令。 我们将F8DFD004变成二进制。 这个使用的32位的Thumb2指令集。 其中b0b11是立即数,这里是4,对应的汇编代码的也是4,这里要注意的是,ARM指令采用流水线机制,当前执行地址A的指令,同时已经在对下一条指令进行译码同时已经在读取下下一条指令:PCA4(ThumbThumb2指令集)。 B12b15是寄存器,这段大小是0XC,对应的寄存器就是 后面16bit除了23位意外,全是固定的,其中‘U’表示无条件执行,这里置为1。 其他的汇编指令对应的机器码也是类似的,值得注意的是,不同的架构对应的机器码也是不同的,这也就回答了为了不同的处理器架构会对应不同的指令集。 有兴趣的可以对比CortexM系列和CortexA系列的的指令集。请参考以下手册: ARMArchitectureReferenceManualARMv7AandARMv7Redition。pdf ARMv7MArchitectureReferenceManual。pdf4反汇编代码全解析 进入debug模式,在View下选择disassemblywindow。 这样就可将机器码和对应的代码对应起来。当程序运行起来了,也就从异常向量表中跳转到ResetHandler中,然后跳转到main函数中,而main函数是在栈中,因此需要设置占空间的起始位置。根据STM32的参考手册,SRAM的其起始地址和大小如下: 因此栈顶为起始位置加上栈的大小即可,只要不超过SRAM即可。 值得注意的是,栈是向低地址扩展的数据结构,是一块连续的内存区域,栈顶的地址和栈的最大容量是在通过LDR设置,因此需要根据应用需求合理分配栈空间。 接下来往下走,如果在汇编中不打断点,会默认进入main函数的一条指令,就从这里分析。为了分析方便,这里还有使用上一节方便出来的文件。 【C代码33行】 从内存地址0x0800017c拷贝数据0x40021018到r3中,也就是r30x0800017c 也就是将pReg指针保存到r3中。 【C代码34行】 这里对应3条指令 首先将r3拷贝到r0中,然后将r0或上1左移3位,也就是ORRr0,r0,8 最后将r0的值写入r3所指地址中。 【C代码37行】 同33行,从内存地址0x08000180拷贝数据40010c00到r3中 【C代码38行】 同34行,这里也对应3条指令: 【C代码40行】 和33行不同的是,这里分了两条指令: 笔者认为前面是编译器优化了。根据ARM指令采用流水线机制,当前执行地址A的指令,同时已经在对下一条指令进行译码同时已经在读取下下一条指令:PCA4(ThumbThumb2指令集)。因此前面类似的代码被优化了。 接下来就进入循环中。 后面就移植在死循环中,不断操作GPIO的亮灭。 【C代码45行】 这里是将B0设置为1,和34行类似。 【C代码47行】 这里将进入延时函数。 进入延时函数: NOP是字节对齐,减少指令的内存访问次数。首先将变量d保存到r0,然后将r0赋给r1,接着是r0自减1,紧接着是r1与0比较,如果r1等于0,则会返回,否则,又从头开始,值得注意的是,这里先比较,然后r0才自减的。 为了进一步说明,可以看d的汇编代码。 这里就是相当于r1先减1,然后再比较的。 【C代码50行】 这行代码对应一下指令,很简单。 5总结 在前面使用Keil进行了反汇编,也对相应的C代码进行了分析。我们看到的反汇编代码如下: 根据反汇编的代码,可将其对应到Flash,在Flash上的内容如下表所示: 地址 Flash内容 0x08000000 00000000hr0x08000004 08000009hr0x08000008 f8dfd004 0x0800000c f000f80c 最后总结下点灯的流程: 第一步:设置栈:CPU会从0x08000000读取值,用来设置SP。 第二步:跳转:CPU从0x08000004得到地址值,根据它的BIT0切换为ARM状态或Thumb状态,然后跳转。 第三步:对于cortexM3M4,它只支持Thumb状态,所以0x08000004上的值bit0必定是1,0x08000004上的值ResetHandler1。从ResetHandler继续执行。 第四步:然后进入到主函数中执行相应C代码