徒手编写了一个 STM8 的反汇编工具
最近打算玩一下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 结构来划分任务:
int decode_instr(unsigned char opcode)
{
switch(opcode>>4)
{
case 1: case 0x0A: case 0x0B: case 0x0C:
case 0x0D: case 0x0E: case 0x0F:
return decode_group1(opcode);
case 0: case 3: case 4: case 6: case 7:
return decode_group2(opcode);
case 5:
if(Prefix==0x72)
return decode_group2(opcode);
else
return decode_5x(opcode);
case 8:
return decode_8x(opcode);
case 2:
return decode_2x(opcode);
case 9:
return decode_9x(opcode);
default:
return -1;
}
}
解码的结果是放到全局变量里面的,返回值只代表了指令是否有效。例如,表格最右边一列的指令我是这样解析的:
int decode_9x(unsigned char opcode)
{
AutoXY=1;
switch(opcode&0x0f)
{
case 0: return set_prefix(0x90);
case 1: return set_prefix(0x91);
case 2: return set_prefix(0x92);
case 3: format(0, LDW, regX, regY);
format(0x90, LDW, regY, regX);
return 1;
case 4: format(0, LDW, regSP, regX);
return 1;
case 5: format(0, LD, regXH, regA);
return 1;
case 6: format(0, LDW, regX, regSP);
return 1;
case 7: format(0, LD, regXL, regA);
return 1;
case 8: format(0, RCF, 0, 0);
return 1;
case 9: format(0, SCF, 0, 0);
return 1;
case 0xA: format(0, RIM, 0, 0);
return 1;
case 0xB: format(0, SIM, 0, 0);
return 1;
case 0xC: format(0, RVF, 0, 0);
return 1;
case 0xD: format(0, NOP, 0, 0);
return 1;
case 0xE: format(0, LD, regA, regXH);
return 1;
case 0xF: format(0, LD, regA, regXL);
return 1;
default:
return -1;
}
}
主要是靠 format() 函数根据当前的指令前缀来翻译操作码:指令名称,寻址的第一操作数、第二操作数。若一共写 256 个 case 分支就太繁琐了,需要抓住共性,像表格中绿色背景的这一组指令我是这么处理的:
int decode_group2(unsigned char opcode)
{
int instr;
AutoXY=1;
switch(opcode&0x0f)
{
case 1:
switch(opcode>>4)
{
case 0: format(0, RRWA, regX, 0); return 1;
case 3: format(0, EXG, regA, longmem); return 1;
case 4: format(0, EXG, regA, regXL); return 1;
case 6: format(0, EXG, regA, regYL); return 1;
default: return -1;
}
break;
case 2:
switch(opcode>>4)
{
case 0: format(0, RLWA, regX, 0); return 1;
case 3: format(0, POP, longmem, 0); return 1;
case 4: format(0, MUL, regX, regA); return 1;
case 6: format(0, DIV, regX, regA); return 1;
case 7: return set_prefix(0x72);
}
break;
case 5:
switch(opcode>>4)
{
case 3: format(0, MOV, longmem, imm8); return 1;
case 4: format(0, MOV, mem, mem); return 1;
case 6: format(0, DIVW, regX, regY); return 1;
default: return -1;
}
break;
case 0xB:
switch(opcode>>4)
{
case 3: format(0, PUSH, longmem, 0); return 1;
case 4: format(0, PUSH, imm8, 0); return 1;
case 6: format(0, LD, offSP, regA); return 1;
case 7: format(0, LD, regA, offSP); return 1;
default: return -1;
}
break;
case 0: instr=NEG; break;
case 3: instr=CPL; break;
case 4: instr=SRL; break;
case 6: instr=RRC; break;
case 7: instr=SRA; break;
case 8: instr=SLL; break;
case 9: instr=RLC; break;
case 0xA:instr=DEC; break;
case 0xC:instr=INC; break;
case 0xD:instr=TNZ; break;
case 0xE:instr=SWAP; break;
case 0xF:instr=CLR; break;
default: return -1;
}
switch(opcode>>4)
{
case 0: format(0, instr, offSP, 0); return 1;
case 3: format(0, instr, mem, 0);
format(0x92, instr, shortptr, 0);
format(0x72, instr, longptr, 0);
return 1;
case 4: format(0, instr, regA, 0);
format(0x72, instr, longoffX, 0);
return 1;
case 5: format(0x72, instr, longmem, 0);
return 1;
case 6: format(0, instr, offX, 0);
format(0x92, instr, sptr_offX, 0);
format(0x72, instr, lptr_offX, 0);
format(0x91, instr, sptr_offY, 0);
return 1;
case 7: format(0, instr, indX, 0);
return 1;
default: return -1;
}
}
在给 format() 这个函数的参数中,指令和操作数类型都是用数值来表示的——用 enum 定义:
#define SI_BASE 100
#define SA_BASE 1000
enum{
ADC=SI_BASE, ADD, ADDW, AND, BCCM, BCP, BCPL, BREAK, BRES, BSET, BTJF, BTJT, CALL,
CALLF, CALLR, CCF, CLR, CLRW, CP, CPW, CPL, CPLW, DEC, DECW, DIV, DIVW, EXG,
EXGW, HALT, INC, INCW, INT, IRET, JP, JPF, JRA,
JRC, JREQ, JRF, JRH, JRIH, JRIL, JRM, JRMI, JRNC, JRNE, JRNH, JRNM, JRNV,
JRPL, JRSGE, JRSGT, JRSLE, JRSLT, JRT, JRUGE, JRUGT, JRULE, JRULT, JRV,
LD, LDF, LDW, MOV, MUL, NEG, NEGW, NOP, OR, POP, POPW, PUSH, PUSHW, RCF, RET,
RETF, RIM, RLC, RLCW, RLWA, RRC, RRCW, RRWA, RVF, SBC, SCF, SIM, SLL, SLLW,
SRA, SRAW, SRL, SRLW, SUB, SUBW, SWAP, SWAPW, TNZ, TNZW, TRAP, WFE, WFI, XOR
};
enum{
regA=SA_BASE, regX, regY, regXH, regXL, regYH, regYL, regCC, regSP,
imm8, imm16, rel, mem, longmem, offX, offY, offSP, longoffX, longoffY,
indX, indY, shortptr, longptr, sptr_offX, sptr_offY, lptr_offX, lptr_offY,
ext, extoffX, extoffY
};
我想这么写而不是直接写字符串的原因是,字符串万一写错了很难检查出来。写成常量编译器可以检查,再统一对应到字符串即可。
format() 函数是这么实现的:
void format(unsigned char pre, int instr, int opr1, int opr2)
{
char replace=(AutoXY && Prefix==0x90 && pre==0);
if(replace)
{
int r1, r2;
r1=replace_X_Y(opr1);
r2=replace_X_Y(opr2);
if(r1>=SA_BASE && r2==0)
opr1=r1;
else
{
if(r2>=SA_BASE && r1==0)
opr2=r2;
else
return;
}
}
if(Prefix==pre ||replace)
{
if(instr
Str_inst="INVALID";
else
Str_inst=SYMI(instr);
if(opr1
Str_opr1=Empty;
else
Str_opr1=SYMA(opr1);
if(opr2
Str_opr2=Empty;
else
Str_opr2=SYMA(opr2);
}
}
format() 函数检查匹配指令前缀,匹配上了才把数值表示的指令和操作数类型转换成字符串,分别存到三个全局变量 Str_inst, Str_opr1, Str_opr2 中,其实这些字符串都是定义好的,也就是写指针而已。有个特殊处理是在 0x90 指令前缀下,自动将 X 寄存器替换为 Y 寄存器。
再来看下主程序中怎么输出反汇编文本的,首先是初始换化几个全局变量,然后调用 decode_instr() 按照操作码分解指令,判断是否成功。如果遇到指令前缀,那么就重新取下一个字节;如果有前缀但指令未被识别,那么调用 decode_instr_special() 进行特殊指令的处理,也就是上面的表中没法表示出来的指令。若解码失败,先输出错误的信息。
for(p=code;p
{
if(Prefix==0)
printf("%5X:\t", base_addr+(p-code));
Str_inst=Empty;
Str_opr1=Empty;
Str_opr2=Empty;
BitOpr=0;
RevOpr=0;
AutoXY=0;
tmp=decode_instr(*p);
if(tmp==0)
{ // prefix set
printf("%02X ", Prefix);
continue;
}
if(tmp>0 && Str_inst==Empty && Prefix)
tmp=decode_instr_special(*p);
if(tmp==-1)
{
if(Prefix==0)
printf(" ");
printf("%02X ", *p);
printf(" ???????? Unknown");
}
下面就是解码成功后的翻译过程了,用 get_extra() 函数从代码数据中提取操作数(立即数、地址等),存放到dat1, dat2两个整型数,供后面用 printf() 输出。至于 printf() 需要的格式字符串,实际上是由解码得到的 Str_opr1, Str_opr2 结果提供的。这里还要特殊处理一下带位操作的指令(BCCM, BCPL, BRES, BSET, BTJF, BTJT这几个),其中的位是编码在操作码当中的,我在 format() 函数中并不把这个位编码作为一个操作数,尽管从汇编语言角度它应该是算一个操作数。
else // OK
{
int nx;
int i;
unsigned int dat1, dat2, arg1, arg2;
char bitpos[]=", #n";
char fmt_str[64];
nx=get_extra(p, &dat1, &dat2);
if(Str_opr1==SYMA(rel))
{
signed char offset=dat1;
dat1=base_addr+(p-code)+nx+1+offset;
}
if(Prefix==0)
printf(" ");
for(i=0;i
printf("%02X ", p[i]);
for(;i
printf(" ");
if(BitOpr)
bitpos[3]="0"+(*p>>1&7);
else
bitpos[0]=0;
if(Str_opr1!=Empty)
{
if(Str_opr2==Empty) // one oprand
{
sprintf(fmt_str, "%s %s%s",Str_inst, Str_opr1, bitpos);
arg1=dat1;
}
else
{
if(RevOpr)
{
sprintf(fmt_str, "%s %s%s, %s",Str_inst, Str_opr2, bitpos, Str_opr1);
if(strchr(Str_opr2,"%"))
{
arg1=dat2;
arg2=dat1;
}
else
arg1=dat1;
}
else
{
sprintf(fmt_str, "%s %s%s, %s",Str_inst, Str_opr1, bitpos, Str_opr2);
if(strchr(Str_opr1,"%"))
{
arg1=dat1;
arg2=dat2;
}
else
arg1=dat2;
}
}
}
else
strcpy(fmt_str, Str_inst);
printf(fmt_str, arg1, arg2);
p+=nx;
}
Prefix=0;
printf("\n");
标签:
-
徒手编写了一个 STM8 的反汇编工具
最近打算玩一下STM8,只为了消化一下我的库存,因为我曾经买过几个型号
-
现在新西兰太不安全了!华人谋杀案!商场炸弹、帮派商场斗殴!
基督城华人失踪案升级谋杀案,警方认为Bao女士“已遇害”!北岛突现炸
-
三十欢(关于三十欢的基本详情介绍)
1、《相见欢》是一部由非天夜翔创作的作品,于晋江文学城首发,小说已
-
白玉菩提根的作用
白玉菩提根是一种稀有的中草药,具有多种药用价值和作用。下面是白玉菩
-
7月28日华商恒益稳健混合净值上涨0.61%
7月28日,截至收盘,华商恒益稳健混合(008488)较前一交易日净值上涨0
-
声迅股份:安防市场是万亿级的大市场 已成为人工智能与实体经济深度融合最成功的应用领域之一
声迅股份近日接受机构调研时表示,安防市场是一个万亿级的大市场,而且
-
成都大运会|今晚,开幕!这些亮点为你揭秘!
成都大运会|今晚,开幕!这些亮点为你揭秘!
-
仕佳光子拟出资1.5亿~2亿元 参与收购美国应用光电公司的中国资产及相关业务
7月28日,河南仕佳光子科技股份有限公司(以下简称仕佳光子)公告,拟
-
《2023中国年轻人塑形与运动白皮书》正式发布
央广网北京7月28日消息随着健康意识的提高,当今年轻人对塑形和运动越
-
危险!特斯拉Autopilot因安全问题将接受调查
据外媒CNBN报道,美国国家交通安全管理局正在对特斯拉多项碰撞事故进行
-
拆解深圳经济半年报,亮点和预期分别是什么?
1 62万亿,同比增长6 3%。这是深圳上半年交出的GDP成绩单。7月27日下午
-
产业观察:汽车流通发展促进绿色循环消费
产业观察:汽车流通发展促进绿色循环消费---发展循环经济是提升资源利
-
陆基条件下典型地物和伪装光谱影响因子分析
引言现代高科技战争伴随着侦察技术与精确制导技术的发展,目标“发现”
-
36岁江疏影是少有的穿搭高手,简约大方有气质,基础款美出高级感
36岁的江疏影是为数不多的会穿高手的人之一,简约大方,基本款好看有高
-
相约大运 成就梦想丨大运会“首金”将在武术项目产生
大运会“首金”将在武术项目产生
-
天风证券涨停
天风证券涨停
-
头盔“新国标”实施近一月 市场监管部门现场抽查头盔质量
丰台区市场监管局提示广大消费者,在选购电动自行车头盔时,要查看产品
-
豪江智能:公司的智能线性驱动控制系统可以应用在机器人领域 但公司尚未在此领域进行业务布局
有投资者在投资者互动平台提问:公司产品如何与马斯克的人形机器人配合
-
NBL阿德莱德36人签下富兰克林 球员曾在CBA砍60+三双&40+四双
澳大利亚国家篮球联赛(NBL)阿德莱德36人官方宣布,正式签下前CBA外援
-
劳模工匠宣讲劳模精神
22日,黑龙江省北戴河劳模疗养院内,进行了一场别开生面的劳模精神宣讲
-
【新闻随笔】点亮更多青少年对天空的梦想
作者:陈城这几天,“地表最强开学典礼”成为网络热议话题。网友们关注的
-
乒乓球世界冠军丁宁分享追梦故事,体育让梦想成真!
为庆祝成都第31届世界大学生夏季运动会召开,全球Z世代体育论坛于7月26
-
东方园林:公司实控人是北京市朝阳区人民政府国有资产监督管理委员会
东方园林(002310)07月28日在投资者关系平台上答复了投资者关心的问题。
-
“文明探源我来说” 文物与文化遗产保护志愿服务宣讲活动在郑州拉开序幕
“这彩陶双连壶是古人做什么用的呀?”“这个壶两边有孔,可以穿绳子,
-
宁波爱柯迪精密部件有限公司(关于宁波爱柯迪精密部件有限公司简述)
,你们好,今天0471房产来聊聊一篇波爱柯迪精密部件有限公司,波爱柯迪
-
公司制企业双重课税是什么意思(公司制企业财务预算包括哪些内容)
来为大家解答以上的问题。公司制企业双重课税是什么意思,公司制企业财
-
湖南推动长江生态保护新观察
近年来,湖南把修复长江生态环境摆在压倒性位置,科学统筹发展与保护,
-
十里南京路,出现过一枚重达70公斤的银行储徽
十里南京路,出现过一枚重达70公斤的银行储徽,储徽,央行,储蓄所,商业银
-
韩国记者曝东电拒绝特定韩媒采访福岛核电站:赤裸裸区别对待
韩国《韩民族日报》驻东京记者金昭延近日在该媒体刊文,曝出东京电力公
-
感受美丽农村路 河北万人大骑游交通寻美活动启动
感受美丽农村路河北万人大骑游交通寻美活动启动