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

原创 电子工程世界 2023-07-26 07:30
最近打算玩一下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点灯测试程序,不包括中断向量表。


 


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


推荐阅读

国内外新增GaN项目盘点
矛头对准自动驾驶汽车,美再向中国发难!
关于芯片战争的二三事
中国半导体行业协会发声:美政府近年采取一系列限制措施令人遗憾

众号内回复您想搜索的任意内容,如问题关键字、技术名词、bug代码等,就能轻松获得与之相关的专业技术内容反馈。快去试试吧!

如果您想经常看到我们的文章,可以进入我们的主页,点击屏幕右上角“三个小点”,点击“设为星标”。


欢迎扫码关注


电子工程世界 关注EEWORLD电子工程世界,即时参与讨论电子工程世界最火话题,抢先知晓电子工程业界资讯。
评论
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 321浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 123浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 194浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 159浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 666浏览
  • 飞凌嵌入式基于瑞芯微RK3562系列处理器打造的FET3562J-C全国产核心板,是一款专为工业自动化及消费类电子设备设计的产品,凭借其强大的功能和灵活性,自上市以来得到了各行业客户的广泛关注。本文将详细介绍如何启动并测试RK3562J处理器的MCU,通过实际操作步骤,帮助各位工程师朋友更好地了解这款芯片。1、RK3562J处理器概述RK3562J处理器采用了4*Cortex-A53@1.8GHz+Cortex-M0@200MHz架构。其中,4个Cortex-A53核心作为主要核心,负责处理复杂
    飞凌嵌入式 2025-01-24 11:21 18浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 617浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 134浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 178浏览
  • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
    虹科Pico汽车示波器 2025-01-23 10:46 74浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦