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代码,只要进入了SoftICESoftICE发回命中断点及断点信息的反馈,然后可以默认发回PCREG等信息,或者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烧写到Block10xE000-0xEFFF 4Kbytes空间,SC[1:0]=0b00,从而使Block1重映射到0x0000-0x1FFF,上电复位后即开始执行SoftICE代码。这便是SoftICE用户指南上说的占用的Block14Kbytes,另外占据的Block00x7C00-0x7FFF 1Kbytes是运行时占用的,不是可执行代码占据的空间。

SoftICE启动后,顺序执行如下:

1.       将内部256Bytes IRAM拷贝到Block00x7D00-0x7DFF空间,这个空间作为USER程序代码IRAM的保存空间;

2.       将Block00x7E00-0x7EFF初始化为零;

3.       将Block10xE054(+0x5F)-0xE0B2的代码拷贝到Block00x7F00(+0x5F)-0x7F5F处;这些代码包括四个部分:MON51入口(0x7F00),MON51串口中断服务程序(0x7F19),FALSH字节编程函数(0x7F2C)FLASH扇区擦除函数(0x7F48)。当编程MON51空间时使用这两个FLASH IAP函数,编程USER空间时使用MON51空间的FLASH IAP函数。

4.       跳转至波特率自适应计算的代码(0xE0B3)执行:PC会向51发送0x11XON),MON51根据这个值计算出波特率;

5.       设置MON51的堆栈SP = 0x07,然后初始化MON51的部分寄存器,已知的MON51的这部分寄存器在地址0x7C88-0x7CA1处,后面会列出这部分寄存器。然后使能所有因单步禁能的断点,如果SERIAL_ISR被修改,则恢复为原来的SERIAL_ISR

6.       然后进入MON51 LOOP接收PC发来的字符,并进行相应的处理。

MON51 LOOP接收的字符及处理方式列出如下:

0x11XON)通信同步信号  MON51采用0x000xFF交替回复PC

0x01      PC要求得到RAM_24(STEP_MODE)的值,与单步执行模式有关,初始化为1,具体作用不详,未从串口收发数据中捕捉到。

上面这两个作为单独字符的消息(暂且将通信数据结构成为消息),下面列出是PCMON51 Firmware消息开头字符,构成MON51的主要处理的消息。他们的消息处理入口表格在0xE919处,将(Code-2A作为索引实现跳转(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的读写内存的命令代码分别为42。下面分别叙述。

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―――修改XRAM0-64K

3―――修改XRAM0-0xFF

4―――修改SFR

5―――修改FLASH(使用C: addr0-0x7BFF & 0xF000-0xFFFF

6―――修改BIT

80――修改FLASH(使用B0keil可能会计算而使用C

81――修改FLASH(使用B1keil可能会计算而使用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―――读取XRAM0-64K

3―――读取XRAM0-0xFF

4―――读取SFR

5―――读取FLASH(使用C: addr0-0xFFFF

6―――读取BIT

80――读取FLASH(使用B0keil可能会计算而使用C

81――读取FLASHSoftICE的处理是按照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 SST89x58RDxV3.4

 

MON51之断点管理

zirconsdu@yahoo.com.cn

MON51断点管理的功能代码是0x06,处理代码入口是0xE692。断点管理主要有SET BPENABLE BPDISABLE BPKILL BP和获得断点状态等。

MON51可以设置10个断点,另外一个隐含断点用于GO UNTIL ADDR使用,用户不能设置这个隐含断点。用于断点管理的数据结构如下:

struct _tagBreakPoint {

byte State;

byte AddrH;

byte AddrL;

byte OpCode;

byte Op1;

byte reserved=0xFF;

} BreakPoint;

其中State0表示断点已被去除,该断点槽位可重新使用,为1表示该槽位已被使用且断点使能,为2表示槽位占用但断点不使能,为3表示进入到用户程序的状态中;AddrH/AddrL是断点的位置,OpCode是断点处的指令代码,Op1是指令操作数第一字节或第二条指令的指令代码(当前一指令为单字节指令时),只保存指令的两个字节,最后reservedFLASH未烧写时的0xFF10个断点的BreakPoint结构位于0x7C06(+6*0xA),一个隐含断点的BreakPoint结构紧随其后。

下面简单分析一下断点的原理:当执行在MON51中,设置软件断点时,用户程序Addr处开始的两个字节被保存到OpCodeOp1中,同时用户指令被替换成0x120x7E,记其后的字节内容为0xXX,则这三个字节构成指令LCALL 0x7EXX,回想0x7E00-0x7EFF256字节的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=2ram7C-ram7F未被使用,如下图:

6

2(ram28)

Ram7C

Ram7D

Ram7E

Ram7F

4

 

      op0-3)的响应如下图所示:

             

6

0/4/5/7(ram28)

No(Ram7C)

Ram26(Ram7D)

DPH(Ram7E)

DPL(Ram7F)

4

 

其中ram280表示操作成功,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命令会使用隐含的软件断点来实现,上下文的切换过程中也会涉及到断点的实现,所以在这简单介绍一下软件断点的一般原理。

对于有专用软件中断指令的CPUMCU,软件断点可以直接使用软件中断来实现,这些软件中断指令一般是单字节指令。设定断点时,断点地址处的指令被替换为软件中断指令,该指令被执行到时便触发中断,在软件中断服务程序中可以切换到调试器进程或上下文,完成后续的调试功能。51单片机没有软件中断功能,所有要实现断点功能,就要采用直接使用LCALL指令跳转到MON51程序的手法来实现。其中RUN UNTIL(汇编指令级)的实现就是将被最后执行到指令的下一条指令用软件断点代替,其对应数据结构保存在隐含断点处,从而达到RUN UNTIL后执行重新进入MON51的目的。

MON51断点的设置、使能、禁止和去除命令格式见MON51断点管理一文。

2源码级调试命令和MON51命令

调试器并不是直接实现C源代码级的调试指令,如STEP INTOSTEP OVERRUN TO CURSORSTEP TO MAIN等,而是根据执行的情景使用适当的机器指令(汇编指令)级的调试命令来实现。MON51的实现就是keil调试器和下位的MON51 firmware共同完成的,这两部分通过AGDI接口来连接,AGDI接口基本上已经比较接近机器指令级调试命令,有的接口是机器指令调试命令的简单组合。

机器指令级调试命令主要是SINGLE STEPRUN & RUN UNTIL,通过这几条指令的组合来实现源码级调试命令。MON51中通用RUN命令的格式为

RUN FROM StartAddr UNTIL EndAddr

就指令组合问题,下面我列举常用源码级调试命令举例,这些例子也是MON51的手法。

C语言函数处做STEP INTO:多条SINGLE STEP指令直至LCALL指令,然后对LCALL指令执行SINGLE STEP,这会使返回地址压栈,然后修改当前PCLCALL的目标地址;

C语言函数处做STEP OVER:根据LCALL指令找到目标地址作为RUN UNTIL命令的StartAddr,将LCALL返回地址作为EndAddr,然后在EndAddr处设隐含断点,转化成RUN UNTIL命令,然后切换回用户上下文执行,直至遇到断点或隐含断点。

普通源码的STEP可以使用多次SINGLE STEP来实现。

       C语言的RUN TO CURSORRUN TO MAIN可以直接转换成RUN UNTIL命令。

      如果SINGLE STEP到断点处,则应该先恢复指令,然后SINGLE STEP,然后恢复断点,然后视情况执行。

      对于细节,不同的调试器实现根据情景会有不同的处理方式。

      3 MON51 RUN & RUN UNTIL命令格式

      对这个命令协议的总结可能会使读者感到遗憾,因为其中两个功能只能通过源码分析,在串口通讯中没有捕捉到,无法对其功能进行验证。

MON518号命令序列格式如图所示:

      

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 EndAddren1=05,使能StartAddr,为0StartAddr字段无效;en2=05,使能EndAddr,为0EndAddr无效;

              1表示获取某些寄存器的值;

              2未捕获到,处理方式是执行单步一次(with RAM22.1 cleared),然后获取寄存器的值;

3未捕获到,处理方式是执行单步一次(with RAM22.1 set

Ram27总是发送1,用途不明。

注:在RUN命令的处理中,RAM22RAM24是两个重要的变量,其中RAM24.2清零决定实时(Realtime)运行,置位则是单步方式(single-step-by-step)运行;RAM22.1置位对LCALL/ACALL指令使用STEP OVER,清零则采用STEP INTORAM22.2暂时不明。RAM24绝大多数时候保持初始化值1RAM22.1绝大多数时候保持清零,RAM22.2绝大多数时候保持置位。通过分析,RAM24.2决定运行方式可能与条件表达式中止用户程序有关,但是SST89E58RD2 SoftICE不能很好地实现条件表达式中止程序运行功能,不能够像软件模拟方式那么正确漂亮,也就无法验证这几个变量的用途,感兴趣的朋友可以换别的版本的MON51验证。通过串口捕获到的8号命令都是次功能号为0001的命令。

[后期补充:]这几个标志位的用途,一个是TRACE功能是否使能,一个是标志是否已对主机做出应答。

      4 MON51上下文切换

      下面将以RUN命令实现为例来描述执行从MON51切换到用户程序,遇到(隐含)断点重新切换回MON51的过程,RAM22.1=0RAM24.2=0就可以很好地保证这个过程。

      A  MON51切换到用户程序

      首先,执行处于MON51循环中,当接收到08命令然后00 01序列时,处理过程的几处关键代码如下:

E58FMonitor_Loop循环体内执行;

接收到08执行

E5A9lcall   Monitor_Main_Function_Dispatch

Monitor_Main_Function_Dispatchjmp @E919[6] =jmp 0xE6DE,然后执行0xE70A分支

E70Elcall   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

EDF2jz    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_CodeMON51的上下文得到保存,用户程序的上下文得到恢复,用户程序的SP和堆栈也得到恢复,所以最后的ret指令将堆栈中的PC弹出,即跳回到用户程序中执行。跳回到用户的什么地方呢?答案在从用户程序执行到LCALL 0x7EXX进入到MON51的上下文切换代码中,在这儿我先给出答案:对于由遇到RUN UNTIL EndAddr命令的LCALL 0x7EXX而进入MON51的情况,返回地址就是EndAddr,这个地址处的代码已经使用隐含断点的breakpoint结构中保存的用户代码恢复。对于普通断点引起的,则是恢复代码后单步执行该指令,然后重新设置断点然后GO。请注意这种通过修改堆栈中PC,然后ret恢复执行的手法,MON51的上下文切换都是这样实现的。

通常断点设置会检查欲设置断点的地址的前后两个字节是否已经有断点,为什么

Lcall Check_if_Bef_Aft_Has_BPEDE0处)后没有使用判断结果呢?不会引起断点的覆盖吗?想一想可能会引起断点覆盖的情景,然后猜测一下是不是断点设置和恢复逆序?查看代码可以发现Program_LCALL_7Exx_in_User_Code_for_enabled_BPs函数和EEFBlcall   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的上下文之后,MON51ret到哪儿呢?我们可能会想到去检查MON51的堆栈,其实只要加一思考就能想到,MON51RET到上下文切换到用户代码前的MON51的最后一个CALL的下一条指令。看到MON51中用红色标出的那些ljmplcall了吗?浏览一下代码,找到MON51切换到用户程序的那个RET0xE06C处,做为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的上下文切换问题,在此就一并分析。

串口中断进入到MON51分析

首先回想MON51初始化时,将其串口中断服务程序拷贝到0x7F19,然后执行用户程序时修改0x23-0x25处指令为LJMP 0x7F19(在SST89E58RD2 SoftICE0x7F19拷贝源是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,  既然已经修改了串口中断服务指向本代码,就表明0x7C731,使能串口中断用户程序,为什么在此还要加以判断处理不使能的情况?这是一种奇怪的逻辑。难道是担心用户在调试过程中修改为不使能串口中断用户程序功能?

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内容处,然后XXXXDPH中,PCLDPL中,三次INC DPTR,只有当XXXXPCH相等且三次INC不会引起DPLDPH进位时才不会有错误,否则便会出现返回地址的错误。为什么要将PC3呢?这是因为Switch_Context_from_User_to_Monitor会将PC再减去3,是和断点的LCALL指令来做统一处理的。

对于上述程序的问题我进行了多次实验,但是没有捕捉到程序跑飞的现象。不得以,我换了思路来验证我的想法,我把EB27处改为加补码0xFA相当于减6,然后EB2AEB2D指令互换,EB32EB35指令互换,然后重新写入SoftICE固件,串口中断用户程序过程中也没有发现错误。关于这一点,希望有兴趣的朋友交流;如果有深入实验的朋友,也希望能够得到您的反馈。

[后期补充:]难道与51的栈增长方向是高地址增长有关?仅留作提示,未作深思验证。

5写在本节结束前

本文主要描述了MON51RUN命令和上下文切换实现,给出了原理的大致轮廓,细节不尽描述,结合本文阅读代码相信更有收获。

本节就到此结束,其中不乏重复罗嗦之处,但是都是对要点的反复解说。

 

MON51之单步分析

zirconsdu@yahoo.com.cn

在写完了MON51的断点和RUN UNTIL后,由于时间的紧张和心情的慵懒,已不想再整理单步的分析。但是想起自己曾经说过要给出这一部分,适逢今晚这么一个无聊的夜晚,就将这一部分写出来,凑全对MON51 Firmware的分析,也算是对前一阶段的学习的一个总结和纪念。

单步的命令号是0x0CChecksum0xF4,就这么两个字节。

主要的处理过程是清除RAM22.2(这个标志现在版本的MON51如何使用,我也不清楚。[后期补充:]现在清楚了,表明是否已对主机做了应答),然后调用单步处理函数Handle_Single_Step_Core[0xED87],该函数返回后将下一条指令的Opcode通过串口返回上位DLL用于指令译码。

Handle_Single_Step_Core[0xED87]函数首先检测RAM24.2看是否需要单步执行,但是从其代码来看,无论RAM24.2置位还是零位,都不会发生跳转而是顺序执行(因为LCALL {RETI}实际没有跳转,也许MON51的不同版本会有不同的处理方式),然后取出当前要单步执行的指令的OpCode,查表(表位置0xE456256条指令,每条对应一个字节),取低两位是指令长度,然后在0xEDBC处调用Execute_the_Single_Step_Classified_Instruction函数进行单步的处理。

在继续讲解之前,我想说明一下0xE456处的这个表格。该表格(姑且称其为InstructMarkTable)对应8051256条指令(注意是机器码指令,而不是汇编助记符指令),其中每个字节的低两位是该指令的长度。8051256条机器指令,那么单步如何实现呢?是分256种情况分别处理吗?这就是InstructMarkTable每个元素高六位的功能。8051的指令按照功能划分,或者说为了单步处理将相同处理类别的指令划分成组,可以分为十一组,如果你对照8051的机器指令表然后对InstructMarkTable表格做一下统计,就会发现其中端倪。在这儿我还是直接给出答案:

十一组指令分别是:

NORMAL(除去以下指令的其余指令)

助记符为AJMP的指令

助记符为ACALL的指令

助记符为LJMP的指令

助记符为LCALL的指令

RET指令

RETI指令

JZJCPSW判断类型条件转移指令

JBJNB等直接寻址位判断条件转移指令

MOVC指令

JMP @A+DPTR指令

InstructMarkTable每个元素的高六位就是这十一组指令处理跳转入口的索引,每种处理对应一条LJMP指令和一条NOP指令,恰好4字节对齐。

下面就继续分析对每组指令的单步处理方式。

1NORMAL指令的单步处理

将该指令拷贝到0x7CF09字节单步执行缓冲区中,然后在其后面加上两条(两条的原因是后面的条件跳转指令使用)LCALL 0x7F00指令,由于指令的最大长度是3字节,所以9字节足够用,一条单步指令加两条LCALL返回指令。将该缓冲区地址做为恢复上下文的PC,然后lcall   Restore_User_Context_n_Execute_User_Code,然后通过LCALL又返回MON51中。Restore_User_Context_n_Execute_User_Code则是同以前的分析,在此不多作解释。

2AJMP指令的单步处理

采用AJMP指令中的相对地址去修正PC,然后将该PC做为新的PC,也就是说AJMP指令的处理是修正PC即可,并不需要真正的执行做为处理。

3ACALL指令的单步处理

ACALL指令如果是Step into,那么只需要将返回地址入栈,然后按照AJMP指令的处理方式处理即可,也不需要真正的执行。而如果是Step over的方式,则要采用单步处理和RUN Until相结合的处理方式来处理,这一部分请有兴趣的读者参照代码自己分析。这部分我也没有发现被执行过,所以我猜测这部分处理可能老版本的MON51的处理方式的遗留代码用于TRACE或者是代码覆盖功能使用,总之是有强制单步机器指令执行的需求的一类功能使用的,欢迎有兴趣的读者和我交流。

      4LJMP指令的单步处理

      直接将目标地址做为恢复上下文的PC使用

      5LCALL指令的单步处理

      将返回地址压栈,然后按照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中,采取的处理方式完全同JZJCPSW判断条件转移指令的处理

10 MOVC的处理

保存寄存器,读出相应代码字节。详见代码。

11 JMP @A+DPTR的处理

保存相应寄存器,计算出相应位置,将新位置做为预执行处的PC即可。详见代码。

至此,MON51的单步执行的处理就分析完了,虽然其中涉及到强迫单步机器指令单步执行的地方没有做分析,但是有兴趣的朋友可以自己通过代码归纳出程序意图。而且上面列出的处理流程也是MON51使用的流程,涉及到强制单步的那些代码在具体调试过程中没有被测试到。[后期补充:]经过以前的几个补充,这几个地方已不再是疑点。

 

结束语

本系列的小文分析了MON51的几个主要的功能实现,其目的是供那些想问“How to的朋友参考,知道mon51仿真器的工作概要。

我本人现在并没有做MON51的意图,当然一个原因是能写得出原理并不一定能写好一个MON51,另一个原因是我还没有想出有什么动力要我去做一个MON51

MON51的功能还是有些限制的,执行起来也比较慢,而且SST SoftICEFlash的修改频率比我在看其代码之前想象的还要高。

OK,就此结束,全文完。