单片机printf函数基于AVR单片机的反汇编及仿真设计与实现1
肖敏,孙伟,杨兴强,张彩明
山东大学计算机科学与技术学院(250061)
E-mail:minmin2008@tom
摘  要:本文在对AVR系列单片机进行研发的过程中,源文件如果不慎丢失会对进一步的开发造成不便,并且单片机硬件仿真系统一般较为耗时、耗材。针对这一系列问题本文设计了一种单片机开发及仿真过程的逆过程算法,即在PC机上使用软件实现对单片机程序的反汇编以及仿真。首先,将单片机程序区中的目标机器码读出转化为BIN文件;然后将BIN文件反汇编为程序源文件;最后,用软件方法控制该单片机程序的执行过程,即在软件环境下模拟程序在AVR单片机系统中的执行过程,从而实现了AVR系列单片机的反汇编及仿真。该算法用标准C语言实现,在恢复源文件、机顶盒研发项目中都取得了良好的效果。
关键字:反汇编;仿真;AVR单片机;mask;加密电视
1 引 言
计算机软件仿真技术凭其有效性、可重复性、经济性受到各行各业的青睐。大到航空航天系统,小到单片
机程序的开发,仿真技术都成为不可或缺的研究手段。在单片机应用开发过程中,一般是由源文件(*.inc、*.asm、*.tab等)经编译生成目标单片机的特殊代码,也就是所谓的目标码,然后就再进行相关的仿真调试工作。仿真调试可分为两大类--芯片级仿真和代码级仿真。芯片级仿真是指使用仿真软件和ICE硬件工具相配合,在实际硬件上进行仿真调试工作;而代码级仿真则完全在计算机上完成,不需要硬件的参与[1]。单片机的反汇编及仿真,则是在知道编译后的目标代码而不知道其程序源代码时较为适用的研究手段。在这里,反汇编是指由目标代码生成源代码的过程;仿真则是用生成的源代码在PC机上进行代码级仿真;整个过程与开发单片机程序的过程是互逆的。在单片机的开发应用中我们会经常碰到这种问题:将功能代码写好并通过仿真器写入单片机的程序区后,在无法获得源代码的情况下,这将会对单片机功能改进和研发造成不便,这就需要一种工具能将编译后的目标文件反汇编成源文件,并希望能看到这个源文件在单片机正常工作时是怎样发挥其功能的,即将反汇编出的程序用软件仿真执行,这就用到上述所说的反汇编及仿真。这种反汇编及仿真也可以应用在对单片机的破解和攻击上,该技术通常使用处理器通信接口并利用协议、加密算法或这些算法中的安全漏洞来进行攻击。但作为一种开发手段,我们可以通过这种方法来寻回丢失的源文件、检测单片机程序的可执行性、单片机的安全性,并可以快速通过软件环境在PC机上进行仿真,达到缩短研发周期,降低研究经费等比较不错的效果。
综上所述,本文设计了一种简易算法来实现AVR系列单片机的反汇编及仿真,而且针对
1本课题得到高等学校博士学科点专项科研基金(项目编号:20020422030)资助
-1-
AVR系列单片机支持C语言编译这一功能,将该算法用标准C语言实现,这样一来,即可载入AVR硬件仿真器中应用,也可在PC机中单独执行。最后,我们分析了算法的复杂度,并通过与其它反汇编系统的比较证明了本算法的实用性和价值。
2 算法设计与实现
2.1 AVR系列单片机
AVR单片机属于精简指令集(RISC)单片机。这种结构使得AVR单片机具有接近1MIPS/MHz的高速处理能力。快速存取RISC寄存器文件由32个通用工作寄存器组成。传统的基于累加器的结构需要大量的程序代码,以实现累计器和存储器之间的数据传输,而32个通用工作寄存器代替累加器,可以避免传统累加器和存储器之间的数据传输造成的瓶颈现象。而且,由于快速访问寄存器文件包含32个8位可单周期访问的通用寄存器,所以在一个时钟周期内,AVR内核应完成如下操作:读取寄存器文件中的2个操作数,执行操作,将结果存回到寄存器文件。此外,AVR采用了Harvard结构,具有独立的数据和程序总线。程序存储器的指令通过以及流水线运行[2]。CPU在执行一条指令的同时读取下一条指令,实现了指令的单时钟周期运行。在对AVR系列单片机进行反汇编及仿真时除了要熟知单片机的指令系统,还要对其结构有足够的认识,这样才能了解反汇编出的源程序是如何在单片机内部协调工作的并将其用软件
方法展示出来。要做到这一点,我们必须将单片机的硬件构成全部用软件方法来重新定义。因此,为了更好的理解这篇文章中的算法及程序,以下给出了,AVR 的内存映像、通用器存器的图示(以ATmage128 Normal Mode为例)。
图2.1内存映像
-2-
图2.2  CPU通用工作寄存器
2.2实现从机器码到AVR指令系统的反汇编
精简指令集RISC是为了提高CPU运行的速度而设计的指令体系,它的关键技术在于流水线操作和采用等长指令体系结构,使一条指令可以在一个单独操作中完成。这使得对AVR的反汇编工程及仿真,变得简洁明了。以ATmega128为例,设计算法实现从机器码到AVR指令系统的反汇编[3]。AVR 的一个指令字为16位或32位,其中大部分的指令为16位。在Atmega128中,共有133条指令,其中只有4条是32位,其余均为16位。因此,我们的基本思想是:
1.从程序区一次读入一个字(16位);
2.从中取出操作码并判断该字是哪条指令,如果是16位的指令,转3;如果是32位的指
令则转4;否则将该字节当作数据或伪指令处理;
3.将此字转换成相应的汇编指令,再从程序区中取下一个字,直到结束。
4.将此字以及下一个字一并转换成相应的汇编指令,再从程序区中取下一个字,直到结束 关键技术:
思想虽简洁明了,对单片机指令系统较熟悉的人可能在实现上也并不困难,但本文提供的算法可使复杂度达到最低。对于步骤2,我们首先定义一个名词mask。对于一条16比特的指令机器码,即16比特全部由0或1组成,其操作码就包含在这16比特之中;而对于一条32比特的指令字,其操作码包含在其前16比特中。 AVR单片机指令系统的每条指令都有其固定的比特组成该指令的操作码,因此无法统一取出。mask的作用就是取出每条指令的操作码,因此每条指令都配有自己的mask。因此,mask也是一串16比特的0、1码,在这16比特中,如果某比特被置1,说明该比特在指令中为操作码;如果某比特被清0,说明该比特在指令中为操作数。例如:对于指令BREQ k (指令功能:如果状态寄存器的零标志
-3-
位被置位,则相对PC值转移k个字),机器码为:1111 00kk kkkk k001,由此我们可以定义该指令的mask为:0xFC07。
有了mask之后,我们首先做一张指令列表,指令系统中每一条指令对应列表中的一项,该项包括指令名称,为指令取出操作码的mask,以及为指令取出操作数的mask。这样我们就可以根据列表方便的判断从程序区读出的机器码属于哪条指令。具体方法:从程序区读出两字节,跟列表第一项的Mask进行按位与操作,若结果恰好与该列表相的操作码相同,那么,这两个字节就属于该列表项的指令。例如:读入的两字节为0xF0A1,当从列表中查询到BREQ时,由于0xF0A1跟0xFC07(BREQ的Mask)按位与的结果是0xF001,即BREQ的操作码,所以0xF0A1应反汇编为指令BREQ。同理,我们类似的取出指令的源操作数和目的操作数。最终的列表形式如下:
Mask,操作码        op[0],op[1]            指令名      指令格式
0xfc00,0x1800, 0x01f0,0x020f, "SUB", "R%0, R%1",
0xf000,0x5000, 0x00f0,0x0f0f,    "SUBI",  "R%0+16, %1",
0xfc00,0x0800, 0x01f0,0x020f, "SBC", "R%0, R%1",
0xf000,0x4000, 0x00f0,0x0f0f, "SBCI",  "R%0+16, %1",
0xff00,0x9700, 0x0030,0x00cf, "SBIW",  "R%0*2+24, %1",
0xfe0f,0x940a, 0x01f0,0x0000, "DEC", "R%0", ……………………
其中,“mask”,“op[0]”,“op[1]”分别为该条指令的操作码掩码、源操作数掩码和目的操作数掩码。
因此,设计算法一如下:
1.从程序区一次读入一个字(16位);
2.将取出的字跟Mask做按位与操作以取出操作码;
3.根据操作码判断该字是哪条指令,如果是16位的指令,转3;如果是32位的指令则转
4;否则将该字节当作数据或伪指令处理;
4.将此字跟op[0]做按位与操作以取出源操作数(假设指令存在源操作数),跟op[1]做按
位与操作以取出目的操作数(假设指令存在目的操作数),从而转换成相应的汇编指令,再从程序区中取下一个字,直到结束。
5.将此字以及下一个字一并跟op[0]做按位与操作以取出源操作数(假设指令存在源操作
数),跟op[1]做按位与操作以取出目的操作数(假设指令存在目的操作数),从而转换成相应的汇编指令,再从程序区中取下一个字,直到结束
-4-
2.3各指令功能的实现
在完成了机器码到汇编指令的反汇编之后,我们还要将每条指令的功能进行仿真。AVR 系列单片机的指令系统主要功能有数值运算、比较和调转、数据传送、位操作和位测试、MCU 控制。因此我们要软件设置相应的寄存器、Flash、E2PROM、SRAM、I/O、中断向量表、堆栈、程序控制器等各部件以配合完成上述功能。为了提高程序的效率和可读性,我们将Flash、E2PROM、SRAM、STACK、I/O寄存器等都定义为相应的数组。通过数组间数据的交换就模拟完成了单片机构件之间的数据交换。
之后,我们对每一条反汇编出来的指令都作以下判断(假定单片机程序处在正常运行状态并开始运行):
z数值操作的结果是否保留
z操作结果是否会影响状态寄存器
z该指令是否与外部设备之间进行数值交换
z是否根据条件进行跳转
z MCU如何控制指令的执行顺序
然后,我再根据以上的判断结果来完善每条指令的具体操作和功能,进而从程序入口开始整体上把握指令的执行顺序。例如:对于指令IN指令,其功能是从I/O口读数,可用C 语言实现如下:
static  U16  INS_IN( void )
{
sprintf(DisBuffer,"%s\tR%d,0x%02X\t\t%s", "IN",op[0],op[1] ,";In  from I/O location");
if(!IsDASM)
{
if (op[1]==0x1d)/*对E2PROM进行读操作,0x1D是E2PROM数据寄存器。应按照该地址寄存器指示的地址取出地址,然后读出数据*/
{
U8  temp;
temp=IO_Register[0x3f];
temp<<=8;
temp+=IO_Register[0x3e];
IO_Register[0x3d]=IEEProm[temp];
printf("%d\t%d\t",temp,IO_Register[0x3d]);
-5-
IO_Register[op[0]]=IO_Register[0x3d];
}
else
if (op[1]==0x73)/*对I2C总线数据读出时,应该按照I2C总线地址寄存器取出其地址,然后读出数据*/
{
U8  temp1;
temp1=IO_Register[0x72];
IO_Register[0x73]=I2CProm[temp1];
IO_Register[op[0]]=IO_Register[0x73];
}
else
IO_Register[op[0]]=IO_Register[op[1]+32];
……………………
2.4仿真过程中应注意的问题及交互技术
按前述步骤只要将各指令功能分别以小函数形式实现,再用主程序对整体程序指针PC 进行控制,依程
序的执行过程分别调用各指令小函数,便可将整个程序在单片机种的执行过程再现。我们采用算法如下(以ATmage128为例):
1)设整体程序计数指针PC-mage128,其初始值赋为程序入口地址并为每个指令函数
定义一个函数指针;
2)以字为单位取机器码,调用算法一;
3)链接函数指针,实现该指令功能;
4)判断该指令是否为双字指令、跳转指令、自程序调用指令、或返回指令。若属于上
述情况指令,在该指令函数中将PC-mage128作适当修改,否则不进行任何修改;
5)PC-mage128++,转步骤2);
6)执行到程序出口时,结束。
这样我们就可以将单片机Flash中的机器码读出,并在PC机上反汇编及仿真该Flash 中的程序。但在整个反汇编及仿真过程中需注意以下几个问题:
¾程序控制指针一定要正确无误;
¾在小函数中要先将反汇编出的指令代码打印出来,再完成指令功能,否则在功能执行完后可能会影响打印结果;
-6-