干货

徒手编写了一个STM8的反汇编工具

分类名:DIY日期:2017-09-21作者:cruelfox 阅读原文
分享到
微博
QQ
微信
LinkedIn
最近打算玩一下STM8, 只为了消化一下我的库存,因为我曾经买过几个型号的STM8单片机,但是一直没用来DIY啥。我对STM8熟悉程度远不如STM32,  后者是流行广泛的ARM核,STM8却是ST独家的架构。

STM8 CPU是在ST7基础上增强,有人说是从6502演变来的,我看倒也不像。学习了一下历史,Motorola的6800演变出来的6805/6811/6809三个分支,以及6502这个与6800有渊源的CPU,从寄存器和指令集上看STM8是和它们有相似之处的,不过差异的地方也很大。作为一个8位MCU,STM8的寻址范围居然达到16M byte(我不信ST会给8位机配上1M以上的ROM或RAM),寻址模式就很多了,间接内存访问比x86都复杂,看惯了RISC的CPU更不能忍。好吧,虽然指令集复杂,STM8的执行速度还快,反正不会纯用汇编来开发。

ST并没有提供STM8的C编译器(汇编器是有的),需要用第三方的。Cosmic C编译器有免费License的版本可以用,这也是ST推荐的,我就装了一个来试。ST官方支持的还有Raisonance的编译器,此外IAR也有STM8的开发环境。
试写了个C程序测试,可以用STVP连接ST-Link下载程序,但我觉得还需要个能反汇编看编译结果的东西。Cosmic工具链里面没有反汇编程序,ST的汇编工具里也没有,STVD既然能跟踪调试应该有,但我没能把它用起来。
干脆自己写一个STM8反汇编工具吧,也练下手怎么写。

先研究下STM8的指令集,这是一种典型变长指令集,除了前缀字节,操作码就在一个字节里面。于是我照着手册统计了一张表出来:

一个字节能表示的范围除了 0x90, 0x91, 0x92, 0x72 用来做指令前缀,其它几乎都用来作操作码了。当然许多指令都有多种寻址模式的(比如加法是谁和谁相加,需要指定),因此用了不止一个操作码。算上寻址模式,256种指令都不够用的,所以STM8靠前面增加前缀字节来扩展。从手册里面截一个例子如下(这是XOR指令的多种编码):

在指令的操作码后面就是提供数据或地址的字节了,长度由操作码加上前缀来决定。

编写反汇编程序就是写一个根据字节数据流的查表过程。上面我做的那个表只是划分了指令的分布,涉及到寻址模式的细节还是得一边写一边查手册。从表上看,操作码的高半字节大概可以把指令划分为几类,再用低半字节去细分指令,于是我的程序解码第一步就是一个 switch-case 结构来划分任务:
  1. int decode_instr(unsigned char opcode)
  2. {
  3.     switch(opcode>>4)
  4.     {
  5.         case 1: case 0x0A: case 0x0B: case 0x0C:
  6.         case 0x0D: case 0x0E: case 0x0F:
  7.             return decode_group1(opcode);
  8.         case 0: case 3: case 4: case 6: case 7:
  9.             return decode_group2(opcode);
  10.         case 5:
  11.             if(Prefix==0x72)
  12.                 return decode_group2(opcode);
  13.             else
  14.                 return decode_5x(opcode);
  15.         case 8:
  16.             return decode_8x(opcode);
  17.         case 2:
  18.             return decode_2x(opcode);
  19.         case 9:
  20.             return decode_9x(opcode);
  21.         default:
  22.             return -1;
  23.     }
  24. }
复制代码


解码的结果是放到全局变量里面的,返回值只代表了指令是否有效。例如,表格最右边一列的指令我是这样解析的:
  1. int decode_9x(unsigned char opcode)
  2. {
  3.     AutoXY=1;
  4.     switch(opcode&0x0f)
  5.     {
  6.         case 0: return set_prefix(0x90);
  7.         case 1: return set_prefix(0x91);
  8.         case 2: return set_prefix(0x92);
  9.         case 3: format(0, LDW, regX, regY);
  10.                 format(0x90, LDW, regY, regX);
  11.                 return 1;
  12.         case 4: format(0, LDW, regSP, regX);
  13.                 return 1;
  14.         case 5: format(0, LD, regXH, regA);
  15.                 return 1;
  16.         case 6: format(0, LDW, regX, regSP);
  17.                 return 1;
  18.         case 7: format(0, LD, regXL, regA);
  19.                 return 1;
  20.         case 8: format(0, RCF, 0, 0);
  21.                 return 1;
  22.         case 9: format(0, SCF, 0, 0);
  23.                 return 1;
  24.         case 0xA: format(0, RIM, 0, 0);
  25.                 return 1;
  26.         case 0xB: format(0, SIM, 0, 0);
  27.                 return 1;
  28.         case 0xC: format(0, RVF, 0, 0);
  29.                 return 1;
  30.         case 0xD: format(0, NOP, 0, 0);
  31.                 return 1;
  32.         case 0xE: format(0, LD, regA, regXH);
  33.                 return 1;
  34.         case 0xF: format(0, LD, regA, regXL);
  35.                 return 1;
  36.         default:
  37.             return -1;
  38.     }
  39. }
复制代码

主要是靠 format() 函数根据当前的指令前缀来翻译操作码:指令名称,寻址的第一操作数、第二操作数。若一共写 256 个 case 分支就太繁琐了,需要抓住共性,像表格中绿色背景的这一组指令我是这么处理的:
  1. int decode_group2(unsigned char opcode)
  2. {
  3.     int instr;
  4.     AutoXY=1;
  5.     switch(opcode&0x0f)
  6.     {
  7.         case 1:
  8.             switch(opcode>>4)
  9.             {
  10.                 case 0: format(0, RRWA, regX, 0); return 1;
  11.                 case 3: format(0, EXG, regA, longmem); return 1;
  12.                 case 4: format(0, EXG, regA, regXL); return 1;
  13.                 case 6: format(0, EXG, regA, regYL); return 1;
  14.                 default: return -1;
  15.             }
  16.             break;
  17.         case 2:
  18.             switch(opcode>>4)
  19.             {
  20.                 case 0: format(0, RLWA, regX, 0); return 1;
  21.                 case 3: format(0, POP, longmem, 0); return 1;
  22.                 case 4: format(0, MUL, regX, regA); return 1;
  23.                 case 6: format(0, DIV, regX, regA); return 1;
  24.                 case 7: return set_prefix(0x72);
  25.             }
  26.             break;
  27.         case 5:
  28.             switch(opcode>>4)
  29.             {
  30.                 case 3: format(0, MOV, longmem, imm8); return 1;
  31.                 case 4: format(0, MOV, mem, mem); return 1;
  32.                 case 6: format(0, DIVW, regX, regY); return 1;
  33.                 default: return -1;
  34.             }
  35.             break;
  36.         case 0xB:
  37.             switch(opcode>>4)
  38.             {
  39.                 case 3: format(0, PUSH, longmem, 0); return 1;
  40.                 case 4: format(0, PUSH, imm8, 0); return 1;
  41.                 case 6: format(0, LD, offSP, regA); return 1;
  42.                 case 7: format(0, LD, regA, offSP); return 1;
  43.                 default: return -1;
  44.             }
  45.             break;
  46.         case 0:  instr=NEG; break;
  47.         case 3:  instr=CPL; break;
  48.         case 4:  instr=SRL; break;
  49.         case 6:  instr=RRC; break;
  50.         case 7:  instr=SRA; break;
  51.         case 8:  instr=SLL; break;
  52.         case 9:  instr=RLC; break;
  53.         case 0xA:instr=DEC; break;
  54.         case 0xC:instr=INC; break;
  55.         case 0xD:instr=TNZ; break;
  56.         case 0xE:instr=SWAP; break;
  57.         case 0xF:instr=CLR; break;
  58.         default: return -1;
  59.     }
  60.     switch(opcode>>4)
  61.     {
  62.         case 0: format(0, instr, offSP, 0); return 1;
  63.         case 3: format(0, instr, mem, 0);
  64.                 format(0x92, instr, shortptr, 0);
  65.                 format(0x72, instr, longptr, 0);
  66.                 return 1;
  67.         case 4: format(0, instr, regA, 0);
  68.                 format(0x72, instr, longoffX, 0);
  69.                 return 1;
  70.         case 5: format(0x72, instr, longmem, 0);
  71.                 return 1;
  72.         case 6: format(0, instr, offX, 0);
  73.                 format(0x92, instr, sptr_offX, 0);
  74.                 format(0x72, instr, lptr_offX, 0);
  75.                 format(0x91, instr, sptr_offY, 0);
  76.                 return 1;
  77.         case 7: format(0, instr, indX, 0);
  78.                 return 1;
  79.         default: return -1;
  80.     }
  81. }
复制代码


在给 format() 这个函数的参数中,指令和操作数类型都是用数值来表示的——用 enum 定义:
  1. #define SI_BASE 100
  2. #define SA_BASE 1000
  3. enum{
  4.     ADC=SI_BASE, ADD, ADDW, AND, BCCM, BCP, BCPL, BREAK, BRES, BSET, BTJF, BTJT, CALL,
  5.     CALLF, CALLR, CCF, CLR, CLRW, CP, CPW, CPL, CPLW, DEC, DECW, DIV, DIVW, EXG,
  6.     EXGW, HALT, INC, INCW, INT, IRET, JP, JPF, JRA,
  7.     JRC, JREQ, JRF, JRH, JRIH, JRIL, JRM, JRMI, JRNC, JRNE, JRNH, JRNM, JRNV,
  8.     JRPL, JRSGE, JRSGT, JRSLE, JRSLT, JRT, JRUGE, JRUGT, JRULE, JRULT, JRV,
  9.     LD, LDF, LDW, MOV, MUL, NEG, NEGW, NOP, OR, POP, POPW, PUSH, PUSHW, RCF, RET,
  10.     RETF, RIM, RLC, RLCW, RLWA, RRC, RRCW, RRWA, RVF, SBC, SCF, SIM, SLL, SLLW,
  11.     SRA, SRAW, SRL, SRLW, SUB, SUBW, SWAP, SWAPW, TNZ, TNZW, TRAP, WFE, WFI, XOR
  12. };

  13. enum{
  14.     regA=SA_BASE, regX, regY, regXH, regXL, regYH, regYL, regCC, regSP,
  15.     imm8, imm16, rel, mem, longmem, offX, offY, offSP, longoffX, longoffY,
  16.     indX, indY, shortptr, longptr, sptr_offX, sptr_offY, lptr_offX, lptr_offY,
  17.     ext, extoffX, extoffY
  18. };
复制代码

我想这么写而不是直接写字符串的原因是,字符串万一写错了很难检查出来。写成常量编译器可以检查,再统一对应到字符串即可。

format() 函数是这么实现的:
  1. void format(unsigned char pre, int instr, int opr1, int opr2)
  2. {
  3.     char replace=(AutoXY && Prefix==0x90 && pre==0);
  4.     if(replace)
  5.     {
  6.         int r1, r2;
  7.         r1=replace_X_Y(opr1);
  8.         r2=replace_X_Y(opr2);
  9.         if(r1>=SA_BASE && r2==0)
  10.             opr1=r1;
  11.         else
  12.         {
  13.             if(r2>=SA_BASE && r1==0)
  14.                 opr2=r2;
  15.             else
  16.                 return;
  17.         }
  18.     }
  19.     if(Prefix==pre ||replace)
  20.     {
  21.         if(instr
  22.             Str_inst="INVALID";
  23.         else
  24.             Str_inst=SYMI(instr);
  25.         if(opr1
  26.             Str_opr1=Empty;
  27.         else
  28.             Str_opr1=SYMA(opr1);
  29.         if(opr2
  30.             Str_opr2=Empty;
  31.         else
  32.             Str_opr2=SYMA(opr2);
  33.     }
  34. }
复制代码

format() 函数检查匹配指令前缀,匹配上了才把数值表示的指令和操作数类型转换成字符串,分别存到三个全局变量 Str_inst, Str_opr1, Str_opr2 中,其实这些字符串都是定义好的,也就是写指针而已。有个特殊处理是在 0x90 指令前缀下,自动将 X 寄存器替换为 Y 寄存器。

再来看下主程序中怎么输出反汇编文本的,首先是初始换化几个全局变量,然后调用 decode_instr() 按照操作码分解指令,判断是否成功。如果遇到指令前缀,那么就重新取下一个字节;如果有前缀但指令未被识别,那么调用 decode_instr_special() 进行特殊指令的处理,也就是上面的表中没法表示出来的指令。若解码失败,先输出错误的信息。
  1.     for(p=code;p
  2.     {
  3.         if(Prefix==0)
  4.             printf("%5X:t", base_addr+(p-code));
  5.         Str_inst=Empty;
  6.         Str_opr1=Empty;
  7.         Str_opr2=Empty;
  8.         BitOpr=0;
  9.         RevOpr=0;
  10.         AutoXY=0;
  11.         tmp=decode_instr(*p);
  12.         if(tmp==0)
  13.         {   // prefix set
  14.             printf("%02X ", Prefix);
  15.             continue;
  16.         }
  17.         if(tmp>0 && Str_inst==Empty && Prefix)
  18.             tmp=decode_instr_special(*p);
  19.         if(tmp==-1)
  20.         {
  21.             if(Prefix==0)
  22.                 printf("   ");
  23.             printf("%02X ", *p);
  24.             printf("   ????????   Unknown");
  25.         }
复制代码

下面就是解码成功后的翻译过程了,用 get_extra() 函数从代码数据中提取操作数(立即数、地址等),存放到dat1, dat2两个整型数,供后面用 printf() 输出。至于 printf() 需要的格式字符串,实际上是由解码得到的 Str_opr1, Str_opr2 结果提供的。这里还要特殊处理一下带位操作的指令(BCCM, BCPL, BRES, BSET, BTJF, BTJT这几个),其中的位是编码在操作码当中的,我在 format() 函数中并不把这个位编码作为一个操作数,尽管从汇编语言角度它应该是算一个操作数。
  1.         else  // OK
  2.         {
  3.             int nx;
  4.             int i;
  5.             unsigned int dat1, dat2, arg1, arg2;
  6.             char bitpos[]=", #n";
  7.             char fmt_str[64];

  8.             nx=get_extra(p, &dat1, &dat2);
  9.             if(Str_opr1==SYMA(rel))
  10.             {
  11.                 signed char offset=dat1;
  12.                 dat1=base_addr+(p-code)+nx+1+offset;
  13.             }

  14.             if(Prefix==0)
  15.                 printf("   ");
  16.             for(i=0;i<1+nx;i++)
  17.                 printf("%02X ", p[i]);
  18.             for(;i<5;i++)
  19.                 printf("   ");

  20.             if(BitOpr)
  21.                 bitpos[3]='0'+(*p>>1&7);
  22.             else
  23.                 bitpos[0]=0;

  24.             if(Str_opr1!=Empty)
  25.             {
  26.                 if(Str_opr2==Empty) // one oprand
  27.                 {
  28.                     sprintf(fmt_str, "%s %s%s",Str_inst, Str_opr1, bitpos);
  29.                     arg1=dat1;
  30.                 }
  31.                 else
  32.                 {
  33.                     if(RevOpr)
  34.                     {
  35.                         sprintf(fmt_str, "%s %s%s, %s",Str_inst, Str_opr2, bitpos, Str_opr1);
  36.                         if(strchr(Str_opr2,'%'))
  37.                         {
  38.                             arg1=dat2;
  39.                             arg2=dat1;
  40.                         }
  41.                         else
  42.                             arg1=dat1;
  43.                     }
  44.                     else
  45.                     {
  46.                         sprintf(fmt_str, "%s %s%s, %s",Str_inst, Str_opr1, bitpos, Str_opr2);
  47.                         if(strchr(Str_opr1,'%'))
  48.                         {
  49.                             arg1=dat1;
  50.                             arg2=dat2;
  51.                         }
  52.                         else
  53.                             arg1=dat2;
  54.                     }
  55.                 }
  56.             }
  57.             else
  58.                 strcpy(fmt_str, Str_inst);

  59.             printf(fmt_str, arg1, arg2);

  60.             p+=nx;
  61.         }
  62.         Prefix=0;
  63.         printf("n");
复制代码


完整的源程序附在后面。贴一个运行的结果:反汇编内容是我写的LED点灯测试程序,不包括中断向量表。


暂时还不能确定指令有没有遗漏,后面边用边检查吧。

此内容由EEWORLD论坛网友cruelfox原创,如需转载或用于商业用途需征得作者同意并注明出处



阅读原文 浏览量:18427 收藏:6
此内容由EEWORLD论坛网友 cruelfox 原创,如需转载或用于商业用途需征 得作者同意并注明出处

上一篇: MCUXpresso的编码和调试
下一篇: 漫话有源滤波器——低通滤波器篇

评论

登录 | 注册 需要登陆才可发布评论    
评论加载中......
viphotman
图片看不到吗?
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2022 EEWORLD.com.cn, Inc. All rights reserved