关于《从单片机指针说到黑客程序》的疑问

时间:2022-08-30 19:43:49
看过《从单片机指针说到黑客程序》感觉作者说的
unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32}; // 复位代码
(*((void (*)())(&rst)))();
反汇编后ljmp到一个奇怪的地址去,不明白作者什么意思。
我的反汇编是把rst的地址传给R1,R2,然后把R1,R2分别传到DPH,DPL,然后CLR A 
JMP @A+DPTR
跳到rst的地址去了,没见到奇怪的地址啊!!!
有人知道的话告诉我啊?!!


原文如下:

从单片机指针说到黑客程序

作者www.1piao.com/wlg.asp
2004年7月的一天,在电子BBS讨论区上溜达,看到一个有趣的帖子,整个帖子内容如下:

纯C51复位功能函数:一个大三学生,让人又爱又怕

现单列复位部分如下:

main()

{

   unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32};  // 复位代码

   (*((void (*)())(rst)))();  // 执行上一行代码,将rst数组当函数调用

}

本来我告诉他嵌入如下代码:

clr a

push acc

push acc

reti

结果他却玩了前面哪一段,而数组rst[]中的内容恰恰是上面的汇编机器码,他的做法是将
rst数组的数据当作代码保存,然后采用绝对地址方式指向该数组,将该数组中的代码当作
函数来运行。居然通过了!

我觉得有问题,我说即使如此,那绝对地址调用也应该写成(*((void (*)())(&rst)))() 
才对呀,结果他反驳说,那样的话,rst的地址就会当成参数传递给这个绝对地址函数,而
实际LJMP调用的地址并非rst的地址,而是一个不确定的地址。于是我按照自己的说法尝试
了一下,看看汇编结果,还真的是将rst的地址传递给了R1 R2,而绝对函数最终LJMP到了
一个莫名其妙的地址上去了,死翘!

看来C真是一匹不容易驾驭的野马,这个大三学生理解力在我之上,我30多岁的人了,干了
这么多年还没他的境界呢,唉,人家才学了几天啊,翻了几天书就这么厉害了,服了!

 

l         首先分析帖子的C语言代码

第一句定义一个数组rst[],数组内数据就是完成复位功能的汇编机器码,具体对应关系
为:clr a == 0xe4、push acc == 0xc0,0xe0、reti ==0x32

第二句是一个函数指针的用法,函数指针用法稍微有点复杂,可参看本人著的书,:),以
下为快速入门讲解。

定义一个返回值是空函数指针的定义形式如下:

void (*p) ( )

当把函数指针赋值后,就能通过函数指针调用函数,调用形式如下,

      (*p) ( );

或等价的简化形式:

p ( );

假设rst就是函数指针,则如下调用形式就可以令单片机复位再起。

(*rst ) ( );  

但可惜,rst不是函数指针,而是数组名,虽然两者都是地址,但不可直接调用数组名。

如同把char型变量a赋值给int型变量b,(int) 表示强制类型转换:

b = (int) a

函数指针的强制类型转换公式如下(C语言的哲学是定义形式和使用一致):

(  (void (*)()  ) rst 

这样经过转换后的rst就可以当作函数指针使用了,简单的调用形式如下:

#define  K     (  (void (*)( )  ) rst

(*K) ( )

或:

(     * (  void (*)( )  )rst      ) ( );

这样的语句就完成复位再启功能了。类型转换符()的优先级跟指针运算符*的优先级相同,
二者的结合方向是自右至左,所以上述语句就能完成复位功能了。保险起见有些程序员常
常喜欢再加个括号:

#define  K     (   (  (void (*)( )  ) rst   )

(*K) ( )



(     *(   (  void (*)( )  )rst   )    ) ( );

 

由于没有输入参数,上述复位代码更严谨的写法是: 

#define  K     (   (  (void (*)(void )  ) rst   )

(*K) ( )



(     *(   (  void (*)(void )  )rst   )    ) ( );

 

l         关于帖子作者的解释

千万不要犯“&rst”形式的错误,对于一维数组而言,数组名rst就代表地址。以下二者等
价,更常用的是等式左边的形式:

rst == &rst[0]

整个函数指针无所谓参数传递,只是把rst当作程序执行地址调用而已,那个学生的解释也
有问题。

还有一点必须提及,不是说能通过编译,甚至生成正确代码,就表示某语句一定是对的。
对很复杂的语句,要考虑到编译器不严格甚至出错的可能性。

 

l         哈佛结构和一个蠕虫病毒

请注意,定义数组rst[]时用了关键字code,这是C51特有的关键字,意味着把数组定义到
程序空间。标准C是没有关键字code的。

哈佛结构和普林斯顿结构:

哈佛结构——程序空间和存储空间分开的。C51算是不太严格的哈佛结构——虽地址线分
开,但数据线没有分开。DSP是增强的哈佛结构。

PC电脑上奔腾CPU是普林斯顿结构——数据空间和程序空间统一编址。

 

如果数组rst[]数据的汇编机器码是删除文件的机器码,这算不算是病毒?

曾经流行过一种蠕虫病毒,其发作机理采取的就是将恶意代码保存成文本文件,然后通过
指针调用执行这个文本,很多杀毒程序也不会查询文本文件。

程序也罢,数据也罢都是二进制形式,如果数据空间和程序空间是统一编码的, 数据当然
可以当作程序运行。

在这一点上,相对而言,哈佛结构的CPU安全性会好一点点。但嵌入式应用少有病毒,一般
不用关心。

 

l         单片机复位的更好方法

帖子中汇编语言解释如下:

clr a                      //清除ACC=0

push acc               //压0到堆栈——8位

push acc               //再压0到堆栈——再8位

reti                        //返回到0地址,从而执行。

帖子作者的这种复位方法比较麻烦,更加简单的复位写法是(摘自《C缺陷与陷阱》):

(     * (  void (*)( )  )0      ) ( );

本句的分析方法同上,但更加精炼,没有多余的汇编语句。

 

上述复位的方法可称为软件复位。

软件复位跟真正上电复位有很大差别:上电复位时大部分寄存器都有确定的复位值;软件
复位则只相当于从0地址开始执行而已,寄存器不会变为确定的复位值。

如果用户要编程实现上电复位这种情况,在程序中不要踢看门狗即可。大部分单片机都有
看门狗吧。

 

l         附录

笔者精于DSP C24xx,但不太懂C51;读者应能从函数指针的定义和引用中看出来,C语言的
设计哲学是使用形式和定义形式一致,虽然这一点饱受质疑。

如果你觉得鸡蛋好吃时,不必认识那只母鸡;但如果你觉得本文不错,请来笔者网站坐坐
吧www.1piao.com/wlg.asp。

40 个解决方案

#1


void main(void)
{
unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32}; // 复位代码
(*((void (*)())(rst)))(); // 执行上一行代码,将rst数组当函数调用
}
反汇编如下:
0000 020003  LJMP   0003H
0003 787F    MOV    R0,#7FH
0005 E4      CLR    A
0006 F6      MOV    @R0,A
0007 D8FD    DJNZ   R0,0006H
0009 758107  MOV    SP,#07H
000C 020015  LJMP   0015H
000F E4      CLR    A
0010 C0E0    PUSH   ACC
0012 C0E0    PUSH   ACC
0014 32      RETI
0015 02000F  LJMP   000FH
0018 00      NOP
0019 FF      MOV    R7,A
001A FF      MOV    R7,A

#2


void main(void)
{
unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32}; // 复位代码
(*((void (*)())(&rst)))(); // 执行上一行代码,将rst数组当函数调用
}

反汇编如下:
0000 020003  LJMP   0003H
0003 787F    MOV    R0,#7FH
0005 E4      CLR    A
0006 F6      MOV    @R0,A
0007 D8FD    DJNZ   R0,0006H
0009 758107  MOV    SP,#07H
000C 02000F  LJMP   000FH
000F 7A00    MOV    R2,#00H
0011 7916    MOV    R1,#16H
0013 02001C  LJMP   001CH
0016 E4      CLR    A
0017 C0E0    PUSH   ACC
0019 C0E0    PUSH   ACC
001B 32      RETI
001C 8A83    MOV    DPH,R2
001E 8982    MOV    DPL,R1
0020 E4      CLR    A
0021 73      JMP    @A+DPTR
0022 21FF    AJMP   01FFH
0024 FF      MOV    R7,A
0025 FF      MOV    R7,A

#3


void main(void)
{
(*(void(*)())0)(); 
}
反汇编如下:
0000 020003  LJMP   0003H
0003 787F    MOV    R0,#7FH
0005 E4      CLR    A
0006 F6      MOV    @R0,A
0007 D8FD    DJNZ   R0,0006H
0009 758107  MOV    SP,#07H
000C 02000F  LJMP   000FH
000F 020000  LJMP   0000H
0012 21FF    AJMP   01FFH
0014 FF      MOV    R7,A
0015 FF      MOV    R7,A

#4


哈佛结构——程序空间和存储空间分开的。C51算是不太严格的哈佛结构——虽地址线分
开,但数据线没有分开。DSP是增强的哈佛结构。

8051典型的X86体系...

郁闷了...

#5


分析的很好,另外,其实在这里楼主用到的&rst的确使这个问题暴露了
最主要的可能还没理解当&用在非简单类型的变量的时候它代表的意思。
考虑这个问题
int a[5] ={0};
&a + 1和&(a + 1), &a[0] + 1
的区别
知道了这个我想上面的问题基本就明了了!

#6


本人不是在说教哦,不过是提醒一下而已,楼主的功力还是在我之上的!

#7


软件复位跟真正上电复位有很大差别:上电复位时大部分寄存器都有确定的复位值;软件
复位则只相当于从0地址开始执行而已,寄存器不会变为确定的复位值。


关于这个问题,好像我说过,在用之前初始化是一个很好的习惯...嘿嘿

#8


o

#9


想法很有意思啊!
不过为什么不能直接JMP呢?
Jmp 0000H

#10


很不错,学习

#11


作者提到的不确定应该是对(*((void   (*)())(&rst)))() 来说传送的是&rst,是一个不确定的地址,;
对(*((void   (*)())(rst)))() 传送的是rst也就是数组的地址,这个是确定的。

#12


我去座座

#13


www.1piao.com/wlg.asp

#14


(*(void(*)())0)(); 
解释如下:

void(*)() //为函数指针,既也是一个调用地址。这里为类型。

(void(*)())0 //将地址0转换成函数指针类类型
(*(void(*)())0)() //调用转换好的指针。同*(void *)()=0; 

//--------------------------------
(*((void(*)())(rst)))();

void(*)() //同上,为函数指针,既也是一个调用地址。
(void(*)())(rst) //(rst)为地址。将地址(rst)转换成函数指针类型
(*((void(*)())(rst)))(); //调用转换好的指针。

#15


好贴,学习!

#16


拜过。

#17


mark

#18


mark

#19


深奥。。

#20


指针还能这样用。。。

#21


软件复位跟真正上电复位有很大差别:上电复位时大部分寄存器都有确定的复位值;软件 
复位则只相当于从0地址开始执行而已,寄存器不会变为确定的复位值。 

如果用户要编程实现上电复位这种情况,在程序中不要踢看门狗即可。大部分单片机都有 
看门狗吧。 
好贴!

#22


受教了

#23


clr   a                                             //清除ACC=0 

push   acc                               //压0到堆栈——8位 

push   acc                               //再压0到堆栈——再8位 

reti                                                 //返回到0地址,从而执行。 

==============================================================
这样的方式依然有问题,如果是在嵌套的中断(高优先级中断)服务程序中跑飞,然后被错误捕捉指令引导到这里来,那么这样的方式,只退出了嵌套的中断(高优先级中断),造成的后果就是“软”复位后,低优先级的中断无响应;正确的做法如下:
MOV DPTR, #NEST
PUSH DPL
PUSH DPH
RETI
NEST: CLR A
PUSH ACC
PUSH ACC
RETI
==============================================================
前四行退出嵌套的中断(如果有的话)并“跳转”到第五行,后四行回到复位向量并退出低优先级中断。
至于第21楼说的问题,完全可以在第一行前面加上关中断的指令和一些初始化指令来实现。

#24


clr   a                                             //清除ACC=0 

push   acc                               //压0到堆栈——8位 

push   acc                               //再压0到堆栈——再8位 

reti                                                 //返回到0地址,从而执行。 

==============================================================
这样的方式依然有问题,如果是在嵌套的中断(高优先级中断)服务程序中跑飞,然后被错误捕捉指令引导到这里来,那么这样的方式,只退出了嵌套的中断(高优先级中断),造成的后果就是“软”复位后,低优先级的中断无响应;正确的做法如下(修改了格式):
==============================================================
    MOV   DPTR,  #NEST
    PUSH   DPL
    PUSH   DPH
    RETI
NEST: CLR   A
    PUSH   ACC
    PUSH   ACC
    RETI
==============================================================
前四行退出嵌套的中断(如果有的话)并“跳转”到第五行,后四行回到复位向量并退出低优先级中断。
至于第21楼说的问题,完全可以在第一行前面加上关中断的指令和一些初始化指令来实现。

#25


可惜啊,看来我的基本功也不够殷实!

#26


void   main(void)
{
unsigned   char   code   rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32};   //   复位代码
(*((void   (*)())(&rst)))();   //   执行上一行代码,将rst数组当函数调用
}

反汇编如下:
0000 020003     LJMP       0003H
0003 787F         MOV         R0,#7FH
0005 E4             CLR         A
0006 F6             MOV         @R0,A
0007 D8FD         DJNZ       R0,0006H
0009 758107     MOV         SP,#07H
000C 02000F     LJMP       000FH
000F 7A00         MOV         R2,#00H
0011 7916         MOV         R1,#16H
0013 02001C     LJMP       001CH
0016 E4             CLR         A
0017 C0E0         PUSH       ACC
0019 C0E0         PUSH       ACC
001B 32             RETI
001C 8A83         MOV         DPH,R2
001E 8982         MOV         DPL,R1
0020 E4             CLR         A
0021 73             JMP         @A+DPTR
0022 21FF         AJMP       01FFH
0024 FF             MOV         R7,A
0025 FF             MOV         R7,A


我想说的是,对于上面的代码来说,JMp并不是跑到什么其他地方去了,我们可以看到A是0,DPTR的值就是0016,也就是对应E4的地方。
对于第2条语句,上面有人说的很清楚了,我就不重复了。

还想补充的是,上面的复位不完善,没有考虑中断和硬件方面的事情。

#27


Mark!

#28


看不懂……

#29


帖子作者的这种复位方法比较麻烦,更加简单的复位写法是(摘自《C缺陷与陷阱》): 

(           *   (     void   (*)(   )     )0             )   (   ); 

本句的分析方法同上,但更加精炼,没有多余的汇编语句。 
这个最好了,呵呵

#30


指针这个东西,我是又爱又恨。。

#31


顶好贴!!!

#32


不错,有道理!!

#33


mark

#34


我还没学汇编语言,只学了C语言啊

#35


受教了,呵呵,真好玩

#36


我喜欢看这样的帖子,深入到汇编级别!

#37


竟然没看懂!!

#38


mark

#39


看看,学学

#40


mark!!

#1


void main(void)
{
unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32}; // 复位代码
(*((void (*)())(rst)))(); // 执行上一行代码,将rst数组当函数调用
}
反汇编如下:
0000 020003  LJMP   0003H
0003 787F    MOV    R0,#7FH
0005 E4      CLR    A
0006 F6      MOV    @R0,A
0007 D8FD    DJNZ   R0,0006H
0009 758107  MOV    SP,#07H
000C 020015  LJMP   0015H
000F E4      CLR    A
0010 C0E0    PUSH   ACC
0012 C0E0    PUSH   ACC
0014 32      RETI
0015 02000F  LJMP   000FH
0018 00      NOP
0019 FF      MOV    R7,A
001A FF      MOV    R7,A

#2


void main(void)
{
unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32}; // 复位代码
(*((void (*)())(&rst)))(); // 执行上一行代码,将rst数组当函数调用
}

反汇编如下:
0000 020003  LJMP   0003H
0003 787F    MOV    R0,#7FH
0005 E4      CLR    A
0006 F6      MOV    @R0,A
0007 D8FD    DJNZ   R0,0006H
0009 758107  MOV    SP,#07H
000C 02000F  LJMP   000FH
000F 7A00    MOV    R2,#00H
0011 7916    MOV    R1,#16H
0013 02001C  LJMP   001CH
0016 E4      CLR    A
0017 C0E0    PUSH   ACC
0019 C0E0    PUSH   ACC
001B 32      RETI
001C 8A83    MOV    DPH,R2
001E 8982    MOV    DPL,R1
0020 E4      CLR    A
0021 73      JMP    @A+DPTR
0022 21FF    AJMP   01FFH
0024 FF      MOV    R7,A
0025 FF      MOV    R7,A

#3


void main(void)
{
(*(void(*)())0)(); 
}
反汇编如下:
0000 020003  LJMP   0003H
0003 787F    MOV    R0,#7FH
0005 E4      CLR    A
0006 F6      MOV    @R0,A
0007 D8FD    DJNZ   R0,0006H
0009 758107  MOV    SP,#07H
000C 02000F  LJMP   000FH
000F 020000  LJMP   0000H
0012 21FF    AJMP   01FFH
0014 FF      MOV    R7,A
0015 FF      MOV    R7,A

#4


哈佛结构——程序空间和存储空间分开的。C51算是不太严格的哈佛结构——虽地址线分
开,但数据线没有分开。DSP是增强的哈佛结构。

8051典型的X86体系...

郁闷了...

#5


分析的很好,另外,其实在这里楼主用到的&rst的确使这个问题暴露了
最主要的可能还没理解当&用在非简单类型的变量的时候它代表的意思。
考虑这个问题
int a[5] ={0};
&a + 1和&(a + 1), &a[0] + 1
的区别
知道了这个我想上面的问题基本就明了了!

#6


本人不是在说教哦,不过是提醒一下而已,楼主的功力还是在我之上的!

#7


软件复位跟真正上电复位有很大差别:上电复位时大部分寄存器都有确定的复位值;软件
复位则只相当于从0地址开始执行而已,寄存器不会变为确定的复位值。


关于这个问题,好像我说过,在用之前初始化是一个很好的习惯...嘿嘿

#8


o

#9


想法很有意思啊!
不过为什么不能直接JMP呢?
Jmp 0000H

#10


很不错,学习

#11


作者提到的不确定应该是对(*((void   (*)())(&rst)))() 来说传送的是&rst,是一个不确定的地址,;
对(*((void   (*)())(rst)))() 传送的是rst也就是数组的地址,这个是确定的。

#12


我去座座

#13


www.1piao.com/wlg.asp

#14


(*(void(*)())0)(); 
解释如下:

void(*)() //为函数指针,既也是一个调用地址。这里为类型。

(void(*)())0 //将地址0转换成函数指针类类型
(*(void(*)())0)() //调用转换好的指针。同*(void *)()=0; 

//--------------------------------
(*((void(*)())(rst)))();

void(*)() //同上,为函数指针,既也是一个调用地址。
(void(*)())(rst) //(rst)为地址。将地址(rst)转换成函数指针类型
(*((void(*)())(rst)))(); //调用转换好的指针。

#15


好贴,学习!

#16


拜过。

#17


mark

#18


mark

#19


深奥。。

#20


指针还能这样用。。。

#21


软件复位跟真正上电复位有很大差别:上电复位时大部分寄存器都有确定的复位值;软件 
复位则只相当于从0地址开始执行而已,寄存器不会变为确定的复位值。 

如果用户要编程实现上电复位这种情况,在程序中不要踢看门狗即可。大部分单片机都有 
看门狗吧。 
好贴!

#22


受教了

#23


clr   a                                             //清除ACC=0 

push   acc                               //压0到堆栈——8位 

push   acc                               //再压0到堆栈——再8位 

reti                                                 //返回到0地址,从而执行。 

==============================================================
这样的方式依然有问题,如果是在嵌套的中断(高优先级中断)服务程序中跑飞,然后被错误捕捉指令引导到这里来,那么这样的方式,只退出了嵌套的中断(高优先级中断),造成的后果就是“软”复位后,低优先级的中断无响应;正确的做法如下:
MOV DPTR, #NEST
PUSH DPL
PUSH DPH
RETI
NEST: CLR A
PUSH ACC
PUSH ACC
RETI
==============================================================
前四行退出嵌套的中断(如果有的话)并“跳转”到第五行,后四行回到复位向量并退出低优先级中断。
至于第21楼说的问题,完全可以在第一行前面加上关中断的指令和一些初始化指令来实现。

#24


clr   a                                             //清除ACC=0 

push   acc                               //压0到堆栈——8位 

push   acc                               //再压0到堆栈——再8位 

reti                                                 //返回到0地址,从而执行。 

==============================================================
这样的方式依然有问题,如果是在嵌套的中断(高优先级中断)服务程序中跑飞,然后被错误捕捉指令引导到这里来,那么这样的方式,只退出了嵌套的中断(高优先级中断),造成的后果就是“软”复位后,低优先级的中断无响应;正确的做法如下(修改了格式):
==============================================================
    MOV   DPTR,  #NEST
    PUSH   DPL
    PUSH   DPH
    RETI
NEST: CLR   A
    PUSH   ACC
    PUSH   ACC
    RETI
==============================================================
前四行退出嵌套的中断(如果有的话)并“跳转”到第五行,后四行回到复位向量并退出低优先级中断。
至于第21楼说的问题,完全可以在第一行前面加上关中断的指令和一些初始化指令来实现。

#25


可惜啊,看来我的基本功也不够殷实!

#26


void   main(void)
{
unsigned   char   code   rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32};   //   复位代码
(*((void   (*)())(&rst)))();   //   执行上一行代码,将rst数组当函数调用
}

反汇编如下:
0000 020003     LJMP       0003H
0003 787F         MOV         R0,#7FH
0005 E4             CLR         A
0006 F6             MOV         @R0,A
0007 D8FD         DJNZ       R0,0006H
0009 758107     MOV         SP,#07H
000C 02000F     LJMP       000FH
000F 7A00         MOV         R2,#00H
0011 7916         MOV         R1,#16H
0013 02001C     LJMP       001CH
0016 E4             CLR         A
0017 C0E0         PUSH       ACC
0019 C0E0         PUSH       ACC
001B 32             RETI
001C 8A83         MOV         DPH,R2
001E 8982         MOV         DPL,R1
0020 E4             CLR         A
0021 73             JMP         @A+DPTR
0022 21FF         AJMP       01FFH
0024 FF             MOV         R7,A
0025 FF             MOV         R7,A


我想说的是,对于上面的代码来说,JMp并不是跑到什么其他地方去了,我们可以看到A是0,DPTR的值就是0016,也就是对应E4的地方。
对于第2条语句,上面有人说的很清楚了,我就不重复了。

还想补充的是,上面的复位不完善,没有考虑中断和硬件方面的事情。

#27


Mark!

#28


看不懂……

#29


帖子作者的这种复位方法比较麻烦,更加简单的复位写法是(摘自《C缺陷与陷阱》): 

(           *   (     void   (*)(   )     )0             )   (   ); 

本句的分析方法同上,但更加精炼,没有多余的汇编语句。 
这个最好了,呵呵

#30


指针这个东西,我是又爱又恨。。

#31


顶好贴!!!

#32


不错,有道理!!

#33


mark

#34


我还没学汇编语言,只学了C语言啊

#35


受教了,呵呵,真好玩

#36


我喜欢看这样的帖子,深入到汇编级别!

#37


竟然没看懂!!

#38


mark

#39


看看,学学

#40


mark!!