MON51通信协议和实现分析v1.2
来源:互联网 发布:数控加工及编程题库 编辑:程序博客网 时间:2024/06/02 08:53
MON51通信协议和实现分析v1.2
---------基于SST SoftICE代码
(大约是08年的工作和文章, 今天整理翻出来, 贴上)
写在前面:
本文是个人在阅读MON51代码后整理所得,由于调试器实现知识不足,加之有猜测之处,因此肯定有很多错误,仅供对MON51有兴趣的朋友参考。任何人可以引用本文无需作者同意(当然注明出处欢迎),但是所做修改需要注明,本人不对由本文衍生出的文章的错误负责。
下面这段是我在看SST SoftICE代码之前对MON51做的想象,为了使对MON51的认识有个大致的轮廓,该段直接引用在此。
**************************************************************************
首先,通过串口能够将程序下载进51单片机。
在下载之前,或下载完成后,在main处做隐式断点,使其跳转到SoftICE代码,这样SoftICE可以通过串口向PC发回反馈,然后在其中循环监测串口,等待主机命令。
PC在下位USER程序不执行时,要么发送继续执行命令,要么查看数据,设置断点,然后下达执行命令,总之这些都是以命令格式通过串口下达给下位,这些使用MON51协议,当下达继续执行的命令后,51继续执行,直至遇到被替换的所谓的软件断点,然后执行权重新到SoftICE代码,只要进入了SoftICE,SoftICE发回命中断点及断点信息的反馈,然后可以默认发回PC,REG等信息,或者PC收到反馈后主动发送获取这些信息的命令,从而被监听在串口上的SoftICE监听到,从而可以反馈回PC。如此往复。
51一旦执行后,不遇断点是不能停止的。这一切都不需要串口的中断使能。因为SoftICE使用查询来做这些,在正常的用户程序中,可以使用与SoftICE相同的串口配置,甚至可以使用串口的中断功能。
但是一旦要求主机能够主动中断51的执行,那么就需要用到串口的中断了,SoftICE,应该修改串口中断向量,指向SoftICE某处,使其能够向主机发回程序停止反馈并串口检测循环,从而表现为程序停止。
断点采用LCALL指令,将原指令地址压栈或保存,在monitor里面可以知道命中断点的地址。
这是基本的实现框架,有了这个框架,剩下的基本就是MON51通信数据结构,以及debugger如何命令序列的次序,51在中断命中后发送数据序列的次序等细节问题了。
*************************************************************************
以后将分章节以SST89E58RD单片机的SoftICE为例介绍MON51的工作原理.
SST SoftICE初始化部分代码分析
zirconsdu@yahoo.com.cn
本章节介绍MON51代码的初始化部分。
SST89E58RD单片机内部FLASH分两个区,Block0容量32Kbytes,地址空间0x0000-0x7FFF; Block1容量8Kbytes,地址空间0xE000-0xFFFF,根据控制位Block1可以重映射到0x0000-0x1FFF。
SoftICE烧写到Block1的0xE000-0xEFFF 4Kbytes空间,SC[1:0]=0b00,从而使Block1重映射到0x0000-0x1FFF,上电复位后即开始执行SoftICE代码。这便是SoftICE用户指南上说的占用的Block1的4Kbytes,另外占据的Block0的0x7C00-0x7FFF 1Kbytes是运行时占用的,不是可执行代码占据的空间。
SoftICE启动后,顺序执行如下:
1. 将内部256Bytes IRAM拷贝到Block0的0x7D00-0x7DFF空间,这个空间作为USER程序代码IRAM的保存空间;
2. 将Block0的0x7E00-0x7EFF初始化为零;
3. 将Block1的0xE054(+0x5F)-0xE0B2的代码拷贝到Block0的0x7F00(+0x5F)-0x7F5F处;这些代码包括四个部分:MON51入口(0x7F00),MON51串口中断服务程序(0x7F19),FALSH字节编程函数(0x7F2C)和FLASH扇区擦除函数(0x7F48)。当编程MON51空间时使用这两个FLASH IAP函数,编程USER空间时使用MON51空间的FLASH IAP函数。
4. 跳转至波特率自适应计算的代码(0xE0B3)执行:PC会向51发送0x11(XON),MON51根据这个值计算出波特率;
5. 设置MON51的堆栈SP = 0x07,然后初始化MON51的部分寄存器,已知的MON51的这部分寄存器在地址0x7C88-0x7CA1处,后面会列出这部分寄存器。然后使能所有因单步禁能的断点,如果SERIAL_ISR被修改,则恢复为原来的SERIAL_ISR;
6. 然后进入MON51 LOOP接收PC发来的字符,并进行相应的处理。
MON51 LOOP接收的字符及处理方式列出如下:
0x11(XON)通信同步信号 MON51采用0x00和0xFF交替回复PC
0x01 PC要求得到RAM_24(STEP_MODE)的值,与单步执行模式有关,初始化为1,具体作用不详,未从串口收发数据中捕捉到。
上面这两个作为单独字符的消息(暂且将通信数据结构成为消息),下面列出是PC到MON51 Firmware消息开头字符,构成MON51的主要处理的消息。他们的消息处理入口表格在0xE919处,将(Code-2)A作为索引实现跳转(LJMP @A+DPTR)。
0x02 修改用户程序内存和寄存器 E919[0] = 0xE5DF
0x04 读用户程序内存和寄存器 E919[2] = 0xE649
0x06 断点管理 E919[4] = 0xE692
0x08 RUN & RUN UNTIL E919[6] = 0xE6DE
0x0A 用某个特定数值填充内存块 E919[8] = 0xE7DD
0x0C 机器指令(汇编指令)单步 E919[A] = 0xE81C
0x0E 设置RAM_24,未捕捉到 E919[C] = 0xE851
0x10 串口中断执行功能和返回MON51 Version功能 E919[E] = 0xE8BB
MON51使用的0x7C00-0x7FFF地址空间列出如下:
7C00(+0xC*6)- 断点管理池
7C70 Saved MON51’s SP
7C71 RCAP2H \ Saved MON51’s
7C72 RCAP2L / Adaptive Baud
7C73 If Serial Interrupt User Code. 0 or 1
7C74 If SERISR has been Modified. 0 or 1
7C75=>23 \
7C76=>24 |-Mon51’s SERISR: [7C75-7C77] = LJMP 0x7F19
7C77=>25 /
7C88 A \
7C89 PSW \
7C8A IE1 |
7C8B DPH |
7C8C DPL |
7C8D PCH \
7C8E PCL / Can Get From the User’s Stack, Pushed by LCALL to mon51
7C8F SP |
7c90 B \
7c91 SCON \
7c92 T2CON / Saved User’s SFRs(7C88-7CA1), Used for Context Switch
7c93 RCAP2H /
7c94 RCAP2L |
7c95 TH2 |
7c96 TL2 |
7C97 SFDT |
7C98 SFAL |
7C99 SFAH |
7CA0 SFCM /
7CA1 SFCF /
7CF0-7CF8 机器指令单步执行缓冲区
7D00-7DFF User’s IRAM 256 Bytes
7E00-7EFF Filled with zeros (NOPs)
7F00-7F5F some Mon51 code copy to here
7F81-7FFF Saved MON51’s first 0x80 Bytes IRAM
MON51对用户内存的读写
zirconsdu@yahoo.com.cn
51单片机因中断或单步进入SoftICE monitor后,MON51驱动DLL通过串口发送给SoftICE firmware的读写内存的命令代码分别为4和2。下面分别叙述。
1内存写和修改PC
上位发出命令序列如图
2
0(ram26)
PCH
PCL
Checksum(ram25)
2
type(ram26)
DPH
DPL
Bytesnum(ram27)
Byte0
Byte1
………
Byte(num-1)
Checksum(ram25)
其中type字段如下:
0―――修改PC(此时后续字段如左图,0x0000做复位处理)
1―――修改IRAM
2―――修改XRAM(0-64K)
3―――修改XRAM(0-0xFF)
4―――修改SFR
5―――修改FLASH(使用C: addr,0-0x7BFF & 0xF000-0xFFFF)
6―――修改BIT
80――修改FLASH(使用B0,keil可能会计算而使用C)
81――修改FLASH(使用B1,keil可能会计算而使用C)
DPH/DPL为首地址,num为要修改的内存字节数,然后SoftICE每接收一个字节即修改内存,循环直至num字节。
Checksum校验和使整个字段加和为0。
MON51 firmware的应答序列如下:
6
error(ram28)
Ram7C
Ram7D
Ram7E
Ram7F
4
error ram7C ram7D ram7E ram7F
OK 0 - - - -
CHECKSUM ERR 2 - - - -
FLASH ERR 3 - - DPH DPL
SoftICE的具体处理流程略去,感兴趣的请参考代码。
2内存读和读回PC
上位发出命令序列如图
4
0(ram26)
Checksum(ram25)
4
type(ram26)
DPH
DPL
Bytesnum(ram27)
Checksum(ram25)
其中type字段如下:
0―――读取PC(此时后续字段如左图)
1―――读取IRAM
2―――读取XRAM(0-64K)
3―――读取XRAM(0-0xFF)
4―――读取SFR
5―――读取FLASH(使用C: addr,0-0xFFFF)
6―――读取BIT
80――读取FLASH(使用B0,keil可能会计算而使用C)
81――读取FLASH(SoftICE的处理是按照C)
DPH/DPL为首地址,num为要读取的内存字节数。
Checksum校验和使整个字段加和为0。
SoftICE循环发送直至num字节作为应答。如下图:
6
2(ram28)
Ram7C
Ram7D
Ram7E
Ram7F
4
2
PCH
PCL
Checksum()
2
Byte0
Byte1
………
Byte(num-1)
Checksum(ram25)
最左图为上位发送序列校验和出错时的SoftICE应答,中图为读取PC时的应答,右图为读取内存和SFR时的应答。
SST SoftICE的处理流程没做绘制。
3特定值填充连续内存
0x0A
value(ram26)
DPH
DPL
NumHiB(ram29)
NumLoB(ram27)
Checksum(ram25)
SoftICE的处理流程和应答格式同内存写。
注:由于上位使用0x10号命令代码取得MON51的版本号时,SoftICE firmware会使用ram7C-ram7F发回版本号,所以ram7C-ram7F如果没有被修改则为MON51版本号,如SoftICE SST89x58RDx为V3.4。
MON51之断点管理
zirconsdu@yahoo.com.cn
MON51断点管理的功能代码是0x06,处理代码入口是0xE692。断点管理主要有SET BP,ENABLE BP,DISABLE BP,KILL BP和获得断点状态等。
MON51可以设置10个断点,另外一个隐含断点用于GO UNTIL ADDR使用,用户不能设置这个隐含断点。用于断点管理的数据结构如下:
struct _tagBreakPoint {
byte State;
byte AddrH;
byte AddrL;
byte OpCode;
byte Op1;
byte reserved=0xFF;
} BreakPoint;
其中State为0表示断点已被去除,该断点槽位可重新使用,为1表示该槽位已被使用且断点使能,为2表示槽位占用但断点不使能,为3表示进入到用户程序的状态中;AddrH/AddrL是断点的位置,OpCode是断点处的指令代码,Op1是指令操作数第一字节或第二条指令的指令代码(当前一指令为单字节指令时),只保存指令的两个字节,最后reserved是FLASH未烧写时的0xFF。10个断点的BreakPoint结构位于0x7C06(+6*0xA),一个隐含断点的BreakPoint结构紧随其后。
下面简单分析一下断点的原理:当执行在MON51中,设置软件断点时,用户程序Addr处开始的两个字节被保存到OpCode和Op1中,同时用户指令被替换成0x12和0x7E,记其后的字节内容为0xXX,则这三个字节构成指令LCALL 0x7EXX,回想0x7E00-0x7EFF为256字节的NOPs,则当运行用户程序执行到断点时会执行LCALL 0x7EXX从而被NOPs捕捉并继续执行到0x7F00,而这正是MON51的程序入口地址,从而执行权再次进入MON51,而且该断点的地址可以从Stack中取到,从而可以判断是那个断点被命中。其实完全可以将第三个字节的0xXX保存到reserved字节,并且改写为0x00。至于具体的上下文的切换留待后续章节介绍。
上位发送给MON51的序列如下图所示
6
op(ram29)
No(ram27)
unknown(ram26)
DPH
DPL
Checksum()
其中op的含义如下
0―――设置断点,
1―――禁止断点
2―――使能断点
3―――去除断点
4―――获取断点信息
no为断点的计数,从0开始计,为0x80时对所有断点操作;
ram26作用不明,未被使用;
DPH/DPL为断点地址;
SoftICE的响应如下:
Checksum出错时,error=2,ram7C-ram7F未被使用,如下图:
6
2(ram28)
Ram7C
Ram7D
Ram7E
Ram7F
4
对op(0-3)的响应如下图所示:
6
0/4/5/7(ram28)
No(Ram7C)
Ram26(Ram7D)
DPH(Ram7E)
DPL(Ram7F)
4
其中ram28为0表示操作成功,OK;
为4表示该地址前或后2字节内已有断点,无法再设断点;
为5表示断点个数已满10个,无法再设置断点;
为7表示no为不正确的断点号码。
ram26未被使用,作用不明。
对op=4的响应如下图:
6
0(ram28)
State(Ram7C)
OpCode(Ram7D)
DPH(Ram7E)
DPL(Ram7F)
4
MON51 RUN & RUN UNTIL实现和上下文切换分析
zirconsdu@yahoo.com.cn
设置断点是调试程序的重要手段之一,本节探讨MON51软件断点和RUN & RUN UNTIL的实现,并分析用户程序和MON51程序的上下文切换。首先将介绍软件断点的一般原理,然后介绍keil常用的源码级调试命令如何转化为MON51的命令,然后给出MON51 RUN & RUN UNTIL命令的格式,最后介绍从MON51返回到用户程序然后遇到断点重新进入MON51这个过程涉及到的上下文切换。
1软件断点一般原理
由于在RUN UNTIL命令会使用隐含的软件断点来实现,上下文的切换过程中也会涉及到断点的实现,所以在这简单介绍一下软件断点的一般原理。
对于有专用软件中断指令的CPU和MCU,软件断点可以直接使用软件中断来实现,这些软件中断指令一般是单字节指令。设定断点时,断点地址处的指令被替换为软件中断指令,该指令被执行到时便触发中断,在软件中断服务程序中可以切换到调试器进程或上下文,完成后续的调试功能。51单片机没有软件中断功能,所有要实现断点功能,就要采用直接使用LCALL指令跳转到MON51程序的手法来实现。其中RUN UNTIL(汇编指令级)的实现就是将被最后执行到指令的下一条指令用软件断点代替,其对应数据结构保存在隐含断点处,从而达到RUN UNTIL后执行重新进入MON51的目的。
MON51断点的设置、使能、禁止和去除命令格式见MON51断点管理一文。
2源码级调试命令和MON51命令
调试器并不是直接实现C源代码级的调试指令,如STEP INTO、STEP OVER、RUN TO CURSOR、STEP TO MAIN等,而是根据执行的情景使用适当的机器指令(汇编指令)级的调试命令来实现。MON51的实现就是keil调试器和下位的MON51 firmware共同完成的,这两部分通过AGDI接口来连接,AGDI接口基本上已经比较接近机器指令级调试命令,有的接口是机器指令调试命令的简单组合。
机器指令级调试命令主要是SINGLE STEP、RUN & RUN UNTIL,通过这几条指令的组合来实现源码级调试命令。MON51中通用RUN命令的格式为
RUN FROM StartAddr UNTIL EndAddr
就指令组合问题,下面我列举常用源码级调试命令举例,这些例子也是MON51的手法。
C语言函数处做STEP INTO:多条SINGLE STEP指令直至LCALL指令,然后对LCALL指令执行SINGLE STEP,这会使返回地址压栈,然后修改当前PC为LCALL的目标地址;
C语言函数处做STEP OVER:根据LCALL指令找到目标地址作为RUN UNTIL命令的StartAddr,将LCALL返回地址作为EndAddr,然后在EndAddr处设隐含断点,转化成RUN UNTIL命令,然后切换回用户上下文执行,直至遇到断点或隐含断点。
普通源码的STEP可以使用多次SINGLE STEP来实现。
C语言的RUN TO CURSOR和RUN TO MAIN可以直接转换成RUN UNTIL命令。
如果SINGLE STEP到断点处,则应该先恢复指令,然后SINGLE STEP,然后恢复断点,然后视情况执行。
对于细节,不同的调试器实现根据情景会有不同的处理方式。
3 MON51 RUN & RUN UNTIL命令格式
对这个命令协议的总结可能会使读者感到遗憾,因为其中两个功能只能通过源码分析,在串口通讯中没有捕捉到,无法对其功能进行验证。
MON51的8号命令序列格式如图所示:
8
func(ram29)
Unknown(ram27)
En1(ram26)
StartAddrHi(DPH)
StartAddrLo(DPL)
En2(R2)
EndAddrHi(R0)
EndAddrLo(R1)
Checksum(ram25)
其中func为次功能号:
0表示RUN FROM StartAddr UNTIL EndAddr;en1=05,使能StartAddr,为0,StartAddr字段无效;en2=05,使能EndAddr,为0,EndAddr无效;
1表示获取某些寄存器的值;
2未捕获到,处理方式是执行单步一次(with RAM22.1 cleared),然后获取寄存器的值;
3未捕获到,处理方式是执行单步一次(with RAM22.1 set)
Ram27总是发送1,用途不明。
注:在RUN命令的处理中,RAM22和RAM24是两个重要的变量,其中RAM24.2清零决定实时(Realtime)运行,置位则是单步方式(single-step-by-step)运行;RAM22.1置位对LCALL/ACALL指令使用STEP OVER,清零则采用STEP INTO;RAM22.2暂时不明。RAM24绝大多数时候保持初始化值1,RAM22.1绝大多数时候保持清零,RAM22.2绝大多数时候保持置位。通过分析,RAM24.2决定运行方式可能与条件表达式中止用户程序有关,但是SST89E58RD2 SoftICE不能很好地实现条件表达式中止程序运行功能,不能够像软件模拟方式那么正确漂亮,也就无法验证这几个变量的用途,感兴趣的朋友可以换别的版本的MON51验证。通过串口捕获到的8号命令都是次功能号为00和01的命令。
[后期补充:]这几个标志位的用途,一个是TRACE功能是否使能,一个是标志是否已对主机做出应答。
4 MON51上下文切换
下面将以RUN命令实现为例来描述执行从MON51切换到用户程序,遇到(隐含)断点重新切换回MON51的过程,RAM22.1=0,RAM24.2=0就可以很好地保证这个过程。
A MON51切换到用户程序
首先,执行处于MON51循环中,当接收到08命令然后00 01序列时,处理过程的几处关键代码如下:
E58F:Monitor_Loop循环体内执行;
接收到08执行
E5A9:lcall Monitor_Main_Function_Dispatch
在Monitor_Main_Function_Dispatch中jmp @E919[6] =jmp 0xE6DE,然后执行0xE70A分支
E70E:lcall Run_User_Program_From_StartAddr_To_EndAddr
; When Go Until the addr, will return to here; ret with SP(Monitor) imp this(*****)
EDC9 Run_User_Program_From_StartAddr_To_EndAddr:
EDF2:jz Run_As_Realtime_Unitl_BP_or_EndAddr_First
EDF7 Run_As_Realtime_Unitl_BP_or_EndAddr_First:
EDF7 lcall Program_LCALL_7Exx_in_User_Code_for_enabled_BPs
EDFC jz Preapre_to_Switch_User_Context
EE0B lcall PrePare_Usr_Context
EE0E ret
其中函数Run_User_Program_From_StartAddr_To_EndAddr的流程图如下所示,当ram24.2=0b1时,采用step-by-step的运行方式时,1处留待后续章节介绍。
Prepare_to_Switch_Context函数主要做下面的事情,后面附其流程图
EEF9 jnc Modify_ISR_SER_Success
EF10 ljmp Restore_User_Context_n_Execute_User_Code
然后Restore_User_Context_n_Execute_User_Code完成真正的上下文切换,通过ret指令返回用户程序执行,其流程图如下
EFCB ljmp 0x7F13
7F13 orl SFCF, #1 [E067]
pop IE1 [E06A]
ret [E06C]
由于在Restore_User_Context_n_Execute_User_Code中MON51的上下文得到保存,用户程序的上下文得到恢复,用户程序的SP和堆栈也得到恢复,所以最后的ret指令将堆栈中的PC弹出,即跳回到用户程序中执行。跳回到用户的什么地方呢?答案在从用户程序执行到LCALL 0x7EXX进入到MON51的上下文切换代码中,在这儿我先给出答案:对于由遇到RUN UNTIL EndAddr命令的LCALL 0x7EXX而进入MON51的情况,返回地址就是EndAddr,这个地址处的代码已经使用隐含断点的breakpoint结构中保存的用户代码恢复。对于普通断点引起的,则是恢复代码后单步执行该指令,然后重新设置断点然后GO。请注意这种通过修改堆栈中PC,然后ret恢复执行的手法,MON51的上下文切换都是这样实现的。
通常断点设置会检查欲设置断点的地址的前后两个字节是否已经有断点,为什么
Lcall Check_if_Bef_Aft_Has_BP(EDE0处)后没有使用判断结果呢?不会引起断点的覆盖吗?想一想可能会引起断点覆盖的情景,然后猜测一下是不是断点设置和恢复逆序?查看代码可以发现Program_LCALL_7Exx_in_User_Code_for_enabled_BPs函数和EEFB处lcall Restore_All_BPs_User_Code函数使用的断点号果然是相反的。
B 用户程序进入到MON51分析
用户程序进入MON51主要有三种方式:遇到断点,单步执行完毕,串口中断用户程序,在这先分析第一种方式,单步在下次介绍,串口中断本文后面分析。
用户程序遇到LCALL 0x7EXX后,我们已经知道这会引起LCALL指令三字节后的返回PC压栈,然后跳转经过NOPs,程序会执行到MON51入口0x7F00.(在SoftICE代码中地址是0xE054,还记得MON51初始化时的四个函数代码拷贝吗?)
E054 Monitor_Code_Entry_LCAL_7F00:
E064 ljmp Switch_Context_from_User_to_Monitor
E222 Save_User_Code_Context:
E29E mov DPTR, #0x7C8D
E2A1 pop ACC ; PCH
E2A3 acall Byte_Program
E2A5 pop ACC ; PCL
E2A7 acall Byte_Program_Inc_DPTR
E2A9 mov A, SP ; Save User's SP
E2E1 Prepare_Context_for_Monitor:
E2F2 mov SP, A ; Restore monitor's SP
E30D lcall Restore_Serial_ISR_Vec
E310 acall Restore_All_BPs
E312 lcall R0R1_from_7C8D8E_Inc_DPTR ; Get_Code_Address_After_LCALL_7F00
E315 lcall XCHG_R0R1_DPTR
E318 lcall Dec_DPTR ; Dec 3 times, DPTR to orig code addr
E31B lcall Dec_DPTR
E31E lcall Dec_DPTR
E321 lcall XCHG_R0R1_DPTR
E324 mov DPTR, #0x7C8D
E327 mov A, R0
E328 acall Byte_Program_Safe
E32A inc DPTR
E32B mov A, R1
E32C acall Byte_Program_Safe
E32E setb SCON.1
E330 ret ; Ret to where??
其中E2A1-E2A7将用户堆栈中的PC弹出,保存到7C8D-7C8E;
E318-E32C将这个PC推前3个字节后保存,即是断点处地址,也是用户程序恢复执行的地址(强调:此时PC已经被弹出,已不在堆栈中,保存在7C8D-7C8E,返回是重新压栈,见Restore_User_Context_n_Execute_User_Code流程图);
E30D处恢复串口中断服务程序;
E310将所有断点处的LCALL 0x7EXX的前两字节用相应Breakpoint结构中保存的指令恢复;
E2A9处保存用户程序堆栈,此时因本次LCALL而导致的压栈已经被弹空;
E2F2处恢复MON51的堆栈。
在保存了User code的上下文和恢复了MON51的上下文之后,MON51会ret到哪儿呢?我们可能会想到去检查MON51的堆栈,其实只要加一思考就能想到,MON51是RET到上下文切换到用户代码前的MON51的最后一个CALL的下一条指令。看到MON51中用红色标出的那些ljmp和lcall了吗?浏览一下代码,找到MON51切换到用户程序的那个RET在0xE06C处,做为PrePare_Usr_Context函数的返回地址,即0xEE0E,执行该处的代码,又是一条RET:
EE0B lcall PrePare_Usr_Context
EE0E ret
执行ret继续向上找调用者,可以追踪到
E70E lcall Run_User_Program_From_StartAddr_To_EndAddr
注意到“A MON51切换到用户程序”中(*****)的注释了吗?
继续执行,0xE718处又是一条ret,同样跟踪,可以找到
E5A9 lcall Monitor_Main_Function_Dispatch ; Response else but XON n 1
E5AC sjmp Monitor_Loop
然后就是进入MON51的循环,至此由用户程序彻底进入MON51,表现为用户程序的中止,MON51接收来自上位的命令,周而复始。大彻大悟了吧?J
本来,本文写到此处就打算结束了,但是考虑到串口中断的篇幅较少,而且同属进入MON51的上下文切换问题,在此就一并分析。
C 串口中断进入到MON51分析
首先回想MON51初始化时,将其串口中断服务程序拷贝到0x7F19,然后执行用户程序时修改0x23-0x25处指令为LJMP 0x7F19(在SST89E58RD2 SoftICE中0x7F19拷贝源是0xE06D处)。当使用串口中断程序执行功能时,0x23-0x25处会被修改在MON51的用户指南上也说的清楚。下面就直接从0xE06D处开始分析。
E06D Monitor_Serial_ISR_7F19:
………
E079 ljmp Monitor_Serial_ISR_Core_Func ; core function
E07C orl SFCF, #1
E07F reti
下面是Monitor_Serial_ISR_Core_Func标号后代码的伪码:
if (0x7C73==0 ) //serial int user code not enabled
reti;
else //serial int user code enabled
{
if (receiveed_char == 0x11 )
reti;
else if (receiveed_char == 0x1B ) //ESC
{
receive one 0x11;
transmit 0xFF;
receive one 0x11;
transmit 0x00;
prepare the stack;
lcall { reti; } //only to exit the interrupt state
//Same to the sequential handle of “B enter mon51 via breakpoint”
ljmp Switch_Context_from_User_to_Monitor
}
}
具体流程读者参考代码。但是有几点是值得商榷的。
a, 既然已经修改了串口中断服务指向本代码,就表明0x7C73是1,使能串口中断用户程序,为什么在此还要加以判断处理不使能的情况?这是一种奇怪的逻辑。难道是担心用户在调试过程中修改为不使能串口中断用户程序功能?
b, 还是这个串口中断用户程序不使能情况的处理,在reti前,少弹出了IE1。初一想,为什么程序有问题,但是调试过程没有问题呢?这是因为当使能串口中断时,不执行这段代码;当不使能串口中断时,无法知道程序是否能够正确执行(希望硬件条件能够显示程序正确执行与否的朋友验证一下,并能反馈给我,或许以后我也会验证一下)。
c, 是这个处理程序中的下面这段代码
EB21 clr IE1.7
EB23 setb SCON.1
EB25 mov A, SP
EB27 add A, #0xF9 ; cur SP minus 7
EB29 xch A, R0
EB2A mov DPH, @R0
EB2C inc R0
EB2D mov DPL, @R0
EB2F inc DPTR
EB30 inc DPTR
EB31 inc DPTR
EB32 mov @R0, DPL
EB34 dec R0
EB35 mov @R0, DPH
EB37 xch A, R0
EB38 pop PSW
EB3A pop DPH
EB3C pop DPL
EB3E pop ACC
EB40 lcall nullsub_1 ;{ reti }
EB43 ljmp Switch_Context_from_User_to_Monitor
执行这段代码前用户堆栈如下图所示
PSW (SP+7)
DPH
DPL
ACC
IE1
PCH
PCL
XXX (SP)
EB40处的 lcall reti仅仅是使MCU退出中断状态,接着会执行下一条指令。
EB43采用与断点切换上下文相同的后续代码来进入MON51。
51堆栈是实栈顶,EB27处加单字节补码0xF9相当于减7,这样SP就到了指向不可预知的XXXX内容处,然后XXXX到DPH中,PCL到DPL中,三次INC DPTR,只有当XXXX与PCH相等且三次INC不会引起DPL向DPH进位时才不会有错误,否则便会出现返回地址的错误。为什么要将PC加3呢?这是因为Switch_Context_from_User_to_Monitor会将PC再减去3,是和断点的LCALL指令来做统一处理的。
对于上述程序的问题我进行了多次实验,但是没有捕捉到程序跑飞的现象。不得以,我换了思路来验证我的想法,我把EB27处改为加补码0xFA相当于减6,然后EB2A和EB2D指令互换,EB32和EB35指令互换,然后重新写入SoftICE固件,串口中断用户程序过程中也没有发现错误。关于这一点,希望有兴趣的朋友交流;如果有深入实验的朋友,也希望能够得到您的反馈。
[后期补充:]难道与51的栈增长方向是高地址增长有关?仅留作提示,未作深思验证。
5写在本节结束前
本文主要描述了MON51的RUN命令和上下文切换实现,给出了原理的大致轮廓,细节不尽描述,结合本文阅读代码相信更有收获。
本节就到此结束,其中不乏重复罗嗦之处,但是都是对要点的反复解说。
MON51之单步分析
zirconsdu@yahoo.com.cn
在写完了MON51的断点和RUN UNTIL后,由于时间的紧张和心情的慵懒,已不想再整理单步的分析。但是想起自己曾经说过要给出这一部分,适逢今晚这么一个无聊的夜晚,就将这一部分写出来,凑全对MON51 Firmware的分析,也算是对前一阶段的学习的一个总结和纪念。
单步的命令号是0x0C,Checksum是0xF4,就这么两个字节。
主要的处理过程是清除RAM22.2(这个标志现在版本的MON51如何使用,我也不清楚。[后期补充:]现在清楚了,表明是否已对主机做了应答),然后调用单步处理函数Handle_Single_Step_Core[0xED87],该函数返回后将下一条指令的Opcode通过串口返回上位DLL用于指令译码。
Handle_Single_Step_Core[0xED87]函数首先检测RAM24.2看是否需要单步执行,但是从其代码来看,无论RAM24.2置位还是零位,都不会发生跳转而是顺序执行(因为LCALL {RETI}实际没有跳转,也许MON51的不同版本会有不同的处理方式),然后取出当前要单步执行的指令的OpCode,查表(表位置0xE456,256条指令,每条对应一个字节),取低两位是指令长度,然后在0xEDBC处调用Execute_the_Single_Step_Classified_Instruction函数进行单步的处理。
在继续讲解之前,我想说明一下0xE456处的这个表格。该表格(姑且称其为InstructMarkTable)对应8051的256条指令(注意是机器码指令,而不是汇编助记符指令),其中每个字节的低两位是该指令的长度。8051有256条机器指令,那么单步如何实现呢?是分256种情况分别处理吗?这就是InstructMarkTable每个元素高六位的功能。8051的指令按照功能划分,或者说为了单步处理将相同处理类别的指令划分成组,可以分为十一组,如果你对照8051的机器指令表然后对InstructMarkTable表格做一下统计,就会发现其中端倪。在这儿我还是直接给出答案:
十一组指令分别是:
NORMAL(除去以下指令的其余指令)
助记符为AJMP的指令
助记符为ACALL的指令
助记符为LJMP的指令
助记符为LCALL的指令
RET指令
RETI指令
JZJC等PSW判断类型条件转移指令
JBJNB等直接寻址位判断条件转移指令
MOVC指令
JMP @A+DPTR指令
InstructMarkTable每个元素的高六位就是这十一组指令处理跳转入口的索引,每种处理对应一条LJMP指令和一条NOP指令,恰好4字节对齐。
下面就继续分析对每组指令的单步处理方式。
1对NORMAL指令的单步处理
将该指令拷贝到0x7CF0的9字节单步执行缓冲区中,然后在其后面加上两条(两条的原因是后面的条件跳转指令使用)LCALL 0x7F00指令,由于指令的最大长度是3字节,所以9字节足够用,一条单步指令加两条LCALL返回指令。将该缓冲区地址做为恢复上下文的PC,然后lcall Restore_User_Context_n_Execute_User_Code,然后通过LCALL又返回MON51中。Restore_User_Context_n_Execute_User_Code则是同以前的分析,在此不多作解释。
2对AJMP指令的单步处理
采用AJMP指令中的相对地址去修正PC,然后将该PC做为新的PC,也就是说AJMP指令的处理是修正PC即可,并不需要真正的执行做为处理。
3对ACALL指令的单步处理
对ACALL指令如果是Step into,那么只需要将返回地址入栈,然后按照AJMP指令的处理方式处理即可,也不需要真正的执行。而如果是Step over的方式,则要采用单步处理和RUN Until相结合的处理方式来处理,这一部分请有兴趣的读者参照代码自己分析。这部分我也没有发现被执行过,所以我猜测这部分处理可能老版本的MON51的处理方式的遗留代码用于TRACE或者是代码覆盖功能使用,总之是有强制单步机器指令执行的需求的一类功能使用的,欢迎有兴趣的读者和我交流。
4对LJMP指令的单步处理
直接将目标地址做为恢复上下文的PC使用
5对LCALL指令的单步处理
将返回地址压栈,然后按照LJMP指令的处理方式继续处理。
6 RET指令的单步处理
地址出栈,修改栈顶
7 RETI指令的单步处理
Lcall RET指令的处理函数
然后reti。
注意:RETI指令的处理本质上是同RET指令的处理的,后面的RETI指令是MON51退回上级调用函数,并且是处理器退出中断状态来实现用户代码的RETI的功能。有那么一点点的小弯,请读者体会。
8 JZJC等条件转移指令
该类型指令是两字节指令,相对偏移位于R5中,处理方式是,首先计算出“若转移被采取”的目标地址保存,然后将R5中填入3,然后按照普通指令拷贝到单步执行缓冲区中按照普通指令的方式单步执行。至此,读者能明白单步执行缓冲区中单步指令后两条LCALL的原因了吧?答案是若转移条件经过执行评估需要“采取转移”,那么就是跳转到后面第二条LCALL指令;若经过执行评估“不采取转移”,那么是直接执行后面第一条LCALL。例如机器码是Opcode[JC] 0x03的指令,03的跳转偏移恰好空过第一条LCALL而执行第二条LCALL。这样就能够在MON51的后续代码中通过LCALL压栈的返回地址知道条件转移是否被采取,从而决定后续的PC指针。
9 JB等位判断条件转移指令
该类型为3字节指令,入口时指令的相对偏移存于R6中,采取的处理方式完全同JZJC等PSW判断条件转移指令的处理
10 MOVC的处理
保存寄存器,读出相应代码字节。详见代码。
11 JMP @A+DPTR的处理
保存相应寄存器,计算出相应位置,将新位置做为预执行处的PC即可。详见代码。
至此,MON51的单步执行的处理就分析完了,虽然其中涉及到强迫单步机器指令单步执行的地方没有做分析,但是有兴趣的朋友可以自己通过代码归纳出程序意图。而且上面列出的处理流程也是MON51使用的流程,涉及到强制单步的那些代码在具体调试过程中没有被测试到。[后期补充:]经过以前的几个补充,这几个地方已不再是疑点。
结束语
本系列的小文分析了MON51的几个主要的功能实现,其目的是供那些想问“How to”的朋友参考,知道mon51仿真器的工作概要。
我本人现在并没有做MON51的意图,当然一个原因是能写得出原理并不一定能写好一个MON51,另一个原因是我还没有想出有什么动力要我去做一个MON51。
MON51的功能还是有些限制的,执行起来也比较慢,而且SST SoftICE对Flash的修改频率比我在看其代码之前想象的还要高。
OK,就此结束,全文完。
- MON51通信协议和实现分析v1.2
- MON51通信协议和实现分析
- adb 通信协议分析以及实现(一)
- IEC 61850通信协议体系介绍和分析
- IEC 61850通信协议体系介绍和分析
- adb 通信协议分析以及实现 (四) ADB shell 命令分析
- adb 通信协议分析以及实现 (四) ADB shell 命令分析
- 053_《Delphi网络通信协议分析与应用实现》
- 电子书下载:Delphi 网络通信协议分析与应用实现
- 远程通信协议分析
- farm通信协议的分析
- 串口通信协议实现
- 网络通信协议的实现
- ATT衰减和通信协议
- 网关设备和通信协议
- MySQL通信协议栈Java实现-(2)协议包格式
- nutch v1.9源码分析(2)——nutch bin和src目录解析及编译
- GAEA Winsieve v1.2 1CD(快速输入和打印结晶粒度分析曲线)
- mysql存储过程详解
- 黑马程序员 19 Java基础加强-01-基础篇
- php文件上传类
- 精通BitmapData
- Linux磁盘自动挂载
- MON51通信协议和实现分析v1.2
- svn merge和branch
- 微软面试题目
- C语言程序设计-学习笔记ch01[未完成]
- 招聘所见思考
- SVN merge 三种方式
- 200行代码搞定炸金花游戏(PHP版)
- JAVA--第四周实验--任务2--求任意整数降序数的程序。(编程思想)
- c++ study