当你编写好程序,编译得到HEX文件,并将HEX下载到MCU中运行。请问MCU中运行的机器代码是100%根据你写的程序转换出来的吗?
你可能不知道,MCU中运行的机器代码已经被偷偷修改了。本文将深入研究我们的代码哪些地方被修改了,然后找到改变我们代码的幕后黑手。
测试环境
软件:keil
硬件:stm32f103
1.反汇编
查看MCU运行的二进制机器代码几乎是不可能完成的任务,那我们是不是没有途径查看查看二进制机器代码?当然不是,我们可以利用反汇编(disassembly)的方法。
将机器代码(二进制代码)转换回人类可读的汇编语言指令的文件,这种转换过程被称为反汇编(disassembly)。
反汇编可以生成一个反汇编文件,反汇编文件是包含汇编语言指令的文本文件。这些指令与原始机器代码在功能上等价,但更易于人类阅读和理解。
生成反汇编文件通常涉及以下几个步骤和工具:
1、获取二进制文件,比如一个可执行文件.exe、.elf、.o等。
2、选择合适的反汇编工具,比如objdump、IDA Pro、Ghidra。
3、使用选择的反汇编工具对二进制文件执行反汇编。
反汇编文件通常包含以下内容:
汇编指令:这是文件的主要部分,包含了从二进制文件反汇编得到的汇编指令。
地址信息:每条汇编指令通常都会附带一个内存地址,表示该指令在原始二进制文件中的位置。
注释和符号:一些反汇编工具可能会尝试解析和恢复原始代码中的符号信息(如函数名、变量名等),并将其作为注释添加到汇编代码中。
节(Section)信息:二进制文件通常被划分为不同的节(如代码节、数据节等),反汇编文件可能会包含这些节的边界和属性信息。
本文我们将使用反汇编的方法得到反汇编文件,通过分析反汇编文件,来检查我们的机器码是不是被修改了。
2.keil生成反汇编
由于我们使用的KEIL集成开发软件内部包含了反编译工具,因此我们只需要在KEIL软件使用fromelf --text 指令,就可以生成反汇编文件。
操作步骤如下:
1、打开Keil工程,点击“Options for Target”,在“Options for
Target”对话框中,选择“User”选项卡。
2、然后勾选“After Build/Rebuild”下的“Run #1”或类似的选项。在“Run #1”的命令框中,输入以下fromelf
命令来生成反汇编文件。
fromelf --text -a -c --output=name1.dis name2.axf
其中name1是生成反汇编文件的名字
其中name2是生成的axf文件的名字(包括文件路径)
3、在“Options for Target”对话框中,找到“Linker”选项卡。 查看Linker control string中的axf文件名称(包括文件路径):…\Output\demo.axf
4、回到“User”选项卡。在“Run #1”的命令框中,修改fromelf 命令
fromelf --text -a -c --output=..\Output\name1.dis ..\Output\demo.axf
(其中…\Output\根据自己工程输出文件路径可定,不同人的工程配置可能不一样)
配置完成后编译工程,就得到了name1.dis反汇编文件,我们可以用记事本或者Notepad++打开.dis反汇编文件(作者更推荐使用Notepad++工具)。
3.增加代码
3.1启动
接下来通过分析反汇编文件,来检查机器码的内容。
为了方便查看,将反汇编文件的后缀名改为.asm,此时用Notepad++工具查看反汇编文件时,会用不同的颜色显示不同格式的内容。
打开反汇编文件,程序空间起始(0x08000000被映射为0x00000000)的第一个32位数为SP的初始值,第二个32位数为PC的初始值(跳转地址)。从反汇编文件可知0x080001e1为跳转地址。
在反汇编文件中找到0x080001e1地址附近,复位处理函数Reset_Handler被执行。
问题:细心的网友可能会发现PC的装载值是0x080001e1,为什么Reset_Handler函数的地址为0x080001e0?
由于crotex-m3内核的MCU运行在Thumb状态下,一些指令更新PC值时,需要将新PC置的LSB置1以表示Thumb状态,否则就会导致错误异常,所以向量表中的每个数值必须将LSB置1。
3.2 scatterload
在Reset_Handler中跳转到0x8000131(实际地址为0x8000130),找到地址0x8000130附近,程序调用了__scatterload函数。
问题来了:在你的程序中有调到到__scatterload函数吗?
实际上,即使你搜索软件工程中的所有文件,都不可能找到scatterload函数,那么scatterload的作用是什么?scatterload函数是如何被添加到机器码中的?
Scatterload是分散加载函数
分散加载是ARM开发中一种重要的内存管理和映像文件生成方法,通过scatter文件(分散加载描述文件)来指定代码和数据在内存中的分布。
在程序执行过程中,__scatterload函数负责将RW/RO输出段从装载域地址复制到运行域地址,并完成ZI运行域的初始化工作。
代码的编程过程如下,在这个过程中的链接环节,链接器将分散加载函数添加到了机器码中。所以机器码已经被编译器修改,增加了scatterload函数到机器码。
我们分析scatterload函数,执行如下指令后
LDR r4,[pc,#24]
LDR r0,[r4,#0xc]
ORR r3,r0,#1
BLX r3
跳转到08000a4a(LSB置1)
3.3 decompress
查看08000a4a代码,执行了一个__decompress解压函数。同样的,即使你搜索软件工程中的所有文件,都不可能找到__decompress函数。和scatterload函数一样,__decompress解压函数也是编译器加到机器码中的。
decompress是用于数据解压,既然有数据解压,那么肯定对应有数据压缩。在《compiler_reference_guide》可以找到关于压缩数据的介绍。根据手册可知:编译器(ARMCC)使用LZ77算法数据压缩。
从《compiler_user_guide》手册中可以找到系统启动初始化流程图如下。
根据启动初始化流程图可知,处理器在启动后执行了cpoy/decompress RW data ,其中cpoy由scatterload函数完成,decompress功能由decompress函数完成。
由此可见,MCU运行的机器码并不是100%由你写的程序转换而来,编译器还在你不知情的情况下增加了一些代码到机器码中。
4.减少代码
4.1 消失的函数
代码中定义了delay_1和delay_2两个延时函数。
在反汇编文件中搜索“delay_”,理论上来说应该搜索到“delay_1”和“delay_2”,但是只搜索到了“delay_2”。明明写了两个延时函数,为什么在机器码中只有一个?
重复未原来在软件中,虽然定义了两个延时函数,但是只有delay_2函数被调用过,而delay_1函数则没有被使用过。在这种情况下,为了节省MCU空间,编译器不会将没有使用的函数编译到机器码中。
4.2 消失的变量
代码中定义了test_buff1和test_buff2两个变量。
在反汇编文件中查找“test_buff”,找到了“test_buff1”和“test_buff2”,但是由于test_buff1没有在程序中使用过,test_buff1的地址设置为了0x00000000,就等效于编译没有将这个变量编译到机器码中(反汇编文件中可以看到有其未使用的变量地址也设置成了0x00000000)
由此可见,MCU运行的机器码并不是100%由你写的程序转换而来,编译器还在你不知情的情况下在机器码中删减了一些代码。
5.修改代码
代码中定义了一个test_fun函数。
在反汇编文件中查找“test_fun”,找到了test_fun函数。但是机器码并没有源代码中abc这3个变量的赋值和运算。仔细查看test_fun函数,不管输入的参数值是多少,返回值都是0,而且函数也不会产生其它“副作用”,可以认为这是一个无用的函数。
很显然,编译器也很聪明,编译检测到了这个无用函数,在生成机器码的时候将其它无用的逻辑全部省略,直接让函数返回0 。
由此可见,MCU运行的机器码并不是100%由你写的程序转换而来,编译器还在你不知情的情况下修改了一些代码。
6.幕后大佬的工作
MCU运行的机器码并不是100%由你写的程序转换而来,有一个幕后大佬,在你不知情的情况下已经大刀阔斧了修改了你的代码。
这个幕后大佬就是编译器,它会对你的代码做如下操作:
1、增加代码
2、删减代码
3、修改代码