打印

从哪里来,到哪里去——全面阐述汇编与C的关系

[复制链接]
楼主: zlg315
手机看帖
扫描二维码
随时随地手机跟帖
41
无论是基于前后台还是操作系统的软件设计,都可以无障碍地被所有的项目复用,从而跨越前后台与基于操作系统编程的鸿沟。我们中间有几个人能够做到将前后台的代码一字不改用于操作系统应用程序之中呢?

很想见识一下呐

使用特权

评论回复
42
zlg315|  楼主 | 2011-2-17 12:48 | 只看该作者
本帖最后由 zlg315 于 2011-2-17 13:09 编辑

我在19楼发的帖子就是这样的例子,你看了没有?

使用特权

评论回复
43
zlg315|  楼主 | 2011-2-17 13:08 | 只看该作者
本帖最后由 zlg315 于 2011-2-17 16:53 编辑

从一个简易的微小内核说开去——全面阐述阐述汇编与C的关系(2)

     关于操作系统的学习,各种各种的图书很多,说句实在话,作者编程的水平不可否认确实很高,但对于初学者来说,没有一本是你完全看得懂的,除非由作者来给“购书者”讲课。

     下面我将介绍一个让初学者看得懂的、全部使用C编程的基于80C51的OS,暂且命名为TinyOS51吧!实际上,只能算是一个简易的微小内核,由于篇幅和时间的限制,在此仅介绍如何实现任务切换所需要的“基本代码”,此时,如果你仅仅是看得懂汇编,则很难将执行程序的细节搞懂,当然,通过我分析之后的效果就完全不一样了,后面你将会看到“看得懂与善用”的差别在哪里?不妨先从C标准库开始介绍!

       <setjmp.h>头文件
     与霸道无比的中止函数abort()和退出函数exit()相比,初看起来使用goto语句处理异常更可行。但不幸的是,goto只能在函数内部跳转,即禁止从一个函数直接跳转到另一个函数。
     为了解决这个限制,标准C函数库提供了setjmp()和longjmp()函数,setjmp()相当于非局部标号,longjmp()函数相当于goto的作用,从而实现了从一个函数直接跳转到另一个函数的缺陷,即非局部跳转,头文件<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型。

     1.非局部远程跳转
     无论什么时候,要想实现非局部跳转,都可以使用<setjmp.h>,头文件<setjmp.h>声明了setjmp与longjmp函数以及类型jmp_buf。头文件提供了以下必须的机制:
     jmp_buf是一个数组类型变量,可将它当作“标号”数据对象类型来看待,用于存放恢复调用环境所需要的上下文信息,比如,堆栈指针的当前位置和函数的返回地址;
  setjmp将程序的上下文信息保存到跳转“缓冲区(jmp_buf类型的数组)”,当稍后调用longjmp时,将保存在缓冲区中的上下文信息作为返回地点标记;
   无论在任何地方调用longjmp,将恢复最后一次由setjmp调用保存在“缓冲区”中的上下文信息,实现非局部远程跳转。

     2. 保存调用环境
     setjmp是C标准库中的一个函数,其格式如下:
     #include <setjmp.h>
     int setjmp(jmp_buf envlronment);
     setjmp使用jmp_buf类型数组envlronment变量记录现在的位置,即变量bp的当前值、堆栈指针的当前值(SP)和函数的返回地址addr15~addr0,供以后longjmp恢复该环境时使用。bp是在SDCC中定义的一个虚拟寄存器,用于简化重入操作。
当setjmp调用直接返回时,则其返回值为0;如果setjmp作为longjmp的执行结果再次返回时,则其返回值是由longjmp的第2个参数值retval指定的,它必须是个非0值。因此通过检查它的返回值,程序可以判断是否调用了longjmp。如果存在多个longjmp,也可以由此判断那个longjmp被调用。

      3.jmp_buf
      由于jmp_buf主要用于保存当前调用的上下文信息,为相应的longjmp调用作为返回地点标记,因此保存在缓冲区jmp_buf中的上下文信息,至少包括变量bp的当前值、堆栈指针的当前值(SP)、高8位返回地址addr15~addr8和低8位返回地址addr7~addr0。其中的bp是在SDCC51中定义的一个虚拟寄存器,用于简化重入操作。而对于用户来说,无需关心和编译器有关的变量bp的变化情况。
     从理论上来讲,jmp_buf还应该保存ACC、B、R0~R7、DPTR等寄存器,但根据SDCC51的函数调用规范约定,当一个外部函数返回时,假设这些寄存器的值均已改变,那么在调用外部函数前,SDCC51会将这些寄存器中有效的数据保存起来,在调用函数后再将它们予以恢复,因此也就没有必要保存这些寄存器了。由于SDCC51编译器将变量bp当作寄存器到处使用,且并未象ACC等寄存器那样受到保护,因此需要将变量bp的值保存起来。

     4. 恢复调用环境
     longjmp也是C标准库中的一个函数,其格式如下:
     #include <setjmp.h>
     int longjmp(jmp_buf envlronment, int retval);
     longjmp表示回到跳转缓冲区jmp_buf类型数组envlronment变量记录的位置,恢复setjmp调用所保存的变量bp的当前值、堆栈指针的当前值(SP)和函数返回地址addr15~addr0,转移到setjmp调用处继续执行。
     longjmp不能让setjmp的返回值为0,如果retval为0,则setjmp的返回值为1;如果retval不为0,则setjmp的返回值为retval。尽管longjmp会导致程序转移,但它和goto又有所不同,其区别如下:
  (1)goto语句不能跳出C语言的当前函数;
  (2)longjmp只能跳回曾经到过的地方。由于在执行setjmp的地方仍留有一个活动过程记录,所以   longjmp更象“回到哪里(go back to)”,而不是“往哪里去(go to)”。longjmp还接受一个额外的整型参数retval并返回它的值,从而知道是由longjmp转移到这里的,还是从上一条语句执行后自然而然来到这里的。
     与此同时,setjmp与longjmp必须协同工作,它们有严格的执行顺序,必须先调用setjmp,然后再调用longjmp,以恢复到先前被保存的“程序执行点”。如果在setjmp调用之前执行longjmp,则程序的执行流程变得不可预见,从而导致程序崩溃而退出。

     5. 范例分析
     setjmp与longjmp在后续的操作系统中将起到至关重要的作用,因此初学者必须搞清楚它们之间的关系,否则将无法掌握操作系统的设计思想和实现机理,下面将通过一个实例来说明setjmp与longjmp的具体作用。

程序清单1.1  非局部跳转控制范例(main.c)
24        #include ".\lib\setjmp.h"     // ""表示使用了自编的,而不是系统的setjmp
29        jmp_buf jbTest;
30        unsigned char       ucSum0;
31        unsigned char       ucSum1;
32        unsigned char       ucSum2;

41        void func0 (void)
42        {
43            ucSum0++;
44            longjmp(jbTest);              // 调用longjimp,其返回值为1,即iRt =1
45            ucSum0++;                     // 程序始终不会执行到这里
46        }
55        void func1 (void)                 // 程序始终不会执行这个函数
56        {
57            ucSum1++;
58        }
67        void func2 (void)
68        {
69            ucSum2++;
70        }
79        void main (void)
80        {
81            int iRt;
82
83            while (1) {
84                iRt = setjmp(jbTest);     // 调用setjmp,其返回值为0,即iRt =0
85                if (iRt == 0) {                             
86                    func0();                    // 如果iRt =0,则调用fun0函数
87                    func1();                    // 程序始终不会执行到这里
88                } else {
89                    func2();
90                }
91            }
92        }

      6. setjmp与longjmp的实现
       setjmplongjmp是标准库函数,这两个函数的具体实现与编译器有很大的关联,由于编译参数不一样,因此其代码也不一样。因为这两个函数的实现与硬件有密切的关系,所以它们往往都是由汇编语言编写的。
    由于所有代码均是基于SDCC51编译器来实现的,为了提高兼容性,所以SDCC51提供的库函数都很复杂。为了简化这两个函数,于是约定以下规则:
  l 限定SDCC51为小模式(--model-small)
  l 限定SDCC51 integerlong库被编译成可重入的 (--int-long-reent)
  l 限定SDCC51所有函数被编译成可重入的(--stack-auto)
  l 修改setjmplongjmp的返回值为char
  l 取消longjmp的第2个参数,当调用longjmp时,则让setjmp的返回值始终为1
    由于制定了以上规则,因此完全可以使用C语言来编写setjmplongjmp了。
      
      7. jmp_buf
    由于程序与硬件的关联性因此常常使用typedef来定义数据类型,但它仅仅为数据类型创建别名而不是创建新的数据类型也不为变量分配空间。在某些方面,typedef类似于宏文本替换,其目的是为了提高程序的可移植性。当将代码移植到不同的平台,要选择正确的类型如shortintlong时,只要在typedef中进行修改即可,无需对每个声明都加以修改。这些声明一般放在文本文件中,比如,setjmp.h,并将其包括(#include)在使用它们的每一个源代码文件中。根据前面jmp_buf的详细介绍,新的jmp_buf定义详见程序清单1.2

程序清单1.2   jmp_buf 定义(setjmp.h)
30    #define __SP_SIZE     1                                                         // 堆栈指针长度
31   #define __BP_SIZE       __SP_SIZE                                      // 编译器虚拟的寄存器,用于重入
32   #define __RET_SIZE    2                                                        // 返回地址长度
34   typedef unsigned char jmp_buf[__RET_SIZE + __SP_SIZE + __BP_SIZE];

    中午了!我还没有吃饭,请大家不要着急,且听下文分解。

使用特权

评论回复
44
老鱼探戈| | 2011-2-17 13:18 | 只看该作者
you are Zhou Ligong?

使用特权

评论回复
45
论坛游客| | 2011-2-17 13:28 | 只看该作者
分层设计呀  大家都是这么搞的

使用特权

评论回复
46
zlg315|  楼主 | 2011-2-17 13:49 | 只看该作者
本帖最后由 zlg315 于 2011-2-17 13:58 编辑

大家都是这样搞的?也许吧!我所知道的,大多数程序员都不知道如何“正确”地分层?以为划分为几个模块那就叫分层,那岂不是笑话吗?请问:层与层是如何隔离的?新的功能是如何动态加载的?
     而事实上,大多数程序员因为出身于电子类专业或半路出家或“自学成才”,受到单片机前后台程序设计思想的影响,在程序中几乎都直接用上层代码调用下层代码的,那不叫严格意义上的分层。因此,你所知道的分层与我所使用的分层是有很大差别的。

     如何复用?不妨举一个例子来说明。对于一个链表,大多数人的做法就是定义一个抽象数据类型,需要什么就定义什么,比如:
typedef  struct   _SingleListNode  {                                // 节点类型定义
             struct  _SingleListNode  *Next;                        // 节点的指针域类型
             int                               data;                          // 节点的数据域类型
}SingleListNode;
      假设这个链表包含3个节点,其存储的数据类型是整型值,那么只要从头节点开始,当指针达到第1个节点时,即可访问存储在第1个节点中的数据,以此类推。当程序访问完最后一个节点时,如果希望访问其它节点,则必须从头指针开始,因为单链表无法从相反的方向进行遍历。
     而程序设计的基本原则就是“编程一次、长期复用”,很显然前面介绍的链表无法做到存放任意类型的数据。当需要改变存放数据的类型时,又需要重新修改并再次编译。其实在C语言中,void指针就是一种很好的选择,它可以指向任意类型的数据,只要在使用时进行强制类型转换,即可避免内存分配。比如:
typedef struct  _SingleListNode{
           struct   _SingleListNode     *next;
           void                                 *data;
}SingleListNode;
      当任意类型的数据都用void指针表示时,其最大的问题是调用者不知道会传递什么类型的数据过来,因为对数据的操作在数据结构和算法代码内部是无法知道的,只有外部的调用者才知道数据的类型和对数据的操作方法。当外部调用者调用数据结构和算法提供接口时,如果接口要用到对数据的操作,则必须由调用者将数据的操作方法告诉接口。
     这样测试程序就好编写了!

使用特权

评论回复
47
zlg315|  楼主 | 2011-2-17 17:08 | 只看该作者

插入图片

本帖最后由 zlg315 于 2011-2-19 04:54 编辑

从简易微小内核说开去——全面阐述汇编与C的关系(3)

     8.setjmp的实现
     setjmp就是将相应的寄存器和返回地址保存到jmp_buf数组类型的jbBuf变量中,即保存的寄存器为变量bp的当前值、堆栈指针SP的当前值、高8位返回地址addr15~addr8和低8位返回地址addr7~addr0。对于80C51系列单片机来说,由于调用函数是使用ACALL或LCALL指令实现的,因此这些指令会将函数的返回地址保存在堆栈中,setjmp()定义详见程序清单1.3。
     一般来说,未初始化的指针,实际上是非法的指针不能使用。但是未初始化的指针完全有可能指向任何地方,则程序无法判断它为非法指针。如果后续的代码忘记初始化pucBuf指针而直接使用它,则完全可能造成程序失败。虽然空指针也是非法指针,但可以通过程序判断后,告诉程序员代码可能有问题。也就是说,如果一开始就将指针初始化为空指针,则可避免程序异常。比如:
     data unsigned char *pucBuf=(data void *)0;              // 定义pucBuf为unsigned char类型指针并初始化为空指针
     由于setjmp不需要在堆栈中保存其它的数据,因此仅需用程序清单1.3((46)、(47))保存返回地址即可,根据约定函数最后返回0(程序清单1.3(48))。
程序清单1.3  setjmp()定义(_setjmp.c)
30   extern unsigned char bp;                  // 编译器为简化重入操作而定义的变量
39   char setjmp (jmp_buf jbBuf)
40   {
41       data unsigned char *pucBuf=(data void *)0; // 指向上下文信息存储位置的指针
42
43     pucBuf    = (data unsigned char *)jbBuf;        // 将jbBuf数组的首地址赋给pucBuf
44     *pucBuf++ = bp;                                           // 保存bp的当前值
45     *pucBuf++ = SP;                                           // 保存SP的当前值
46     *pucBuf++ = *(( data unsigned char *)SP);   // 保存返回地址的高8位
47     *pucBuf   = *(( data unsigned char *)((char)(SP - 1)));// 保存返回地址的低8位
48      return 0;
49   }
     下面不妨以“iRt = setjmp(jbTest);”为例,详细说明setjmp()的使用。其执行过程分两步,第一步,调用“setjmp()”函数,第二步,将函数的返回值赋给iRt变量。
     setjmp()函数的执行过程详见图 1.1,分别用“实线”、“虚线”和“点划线”表示。对于SDCC51来说,编译器将调用“setjmp();”函数的语句编译成:
     LCALL   _setjmp;
         
     “实线”表示此时各个成员指向的位置
     当执行“LCALL  _setjm”指令后,单片机将“返回地址”保存到单片机内部SP指向的idata位置,且跳转到程序清单1.3(43),使pucBuf指向jbBuf数组的首地址。     
   
     “虚线”表示setjmp()函数拷贝数据的过程
     ① 对应程序清单1.3(44),将bp的当前值保存到jbBuf中;
     ② 对应程序清单1.3(45),将单片机内部SP的值保存到jbBuf中;
     ③ 对应程序清单1.3((46)、(47)),将返回地址保存到jbBuf中。
     对于SDCC51来说,“return 0”被SDCC编译成:
     MOV   DPL, #0x0                               ;SDCC用DPL保存char类型返回参数
     RET        
     
     “点划线”表示程序执行相应的拷贝步骤后,各个成员指向的新位置
     ⑵ 对应程序清单1.3(45),⑶ 对应程序清单1.3((46)、(47))。
     然后将setjmp()的返回值保存到变量iRt中,即“iRt = setjmp();”。对于SDCC51来说,编译器将这条C语句编译成:
      ;参数赋值语句
      LCALL  _setjmp
      MOV     A, DPL                                  ;SDCC用DPL保存char类型返回参数
    “返回地址”指向“MOV        A, DPL”这条语句,即“点划线⑶”指向的位置。
      1.jpg (277.36 KB)

使用特权

评论回复
48
程序匠人| | 2011-2-17 19:48 | 只看该作者
要细细拜读周工的大作!:)

使用特权

评论回复
49
xyz_boy| | 2011-2-17 22:44 | 只看该作者
先留个记号,慢慢细读体会

使用特权

评论回复
50
流行音乐| | 2011-2-18 09:26 | 只看该作者
一般来说,作老板的用不着精通技术,没作老板的才需要精通技术。

使用特权

评论回复
51
zlg315|  楼主 | 2011-2-18 16:33 | 只看该作者
是否精通技术与是不是老板没有半点关系,关键在于个人的追求和修为。

如果晚上不学习,那也是一生;十年或二十年下来,如果你将每天学习3小时,当做业余生活的一部分,那么,你想不精通也难!久而久之就像闲庭信步一样水到渠成,因此,一个人的成长并不需要刻意而为。

使用特权

评论回复
52
NE5532| | 2011-2-18 21:53 | 只看该作者
周工,转我一位老师看了这个帖子后的一段话,没准你们认识哈

“看了汇编和C的帖子,很不错。不同工具没有高下好坏,只有适合不适合;会用各种工具是技术能力,知道该用那种工具是智慧。另外别人做的操作平台硬件驱动底层无法评估可靠性,君不见,WINDOWS补丁打了多少?哪个操作系统没死过机?程序编写也是要分层次的,调度类程序是功能性的,执行类程序直接影响可靠性!”:lol

使用特权

评论回复
评分
参与人数 1威望 +1 收起 理由
123jj + 1
53
zlg315|  楼主 | 2011-2-19 01:49 | 只看该作者
很好!不知道你的老师是哪一位先生?不妨发一个短信私下告诉我。

使用特权

评论回复
54
歪 歪| | 2011-2-19 03:56 | 只看该作者
汇编与C的关系讲得比较好的最权威的书莫过于:
John R. Levine的《Linker & Loaders》
中国作者写的一本是
《程序员的自我修养---链接、装载与库》

详细讲解了:静态链接、装载与动态链接、运行时库实现,
基于X86体系,分别介绍Windows和Linux下的具体实现。
说明了C、C++启动汇编代码,命令行参数传递等。

而对于分层设计,最好看潘爱民翻译编著的
《COM本质论》
《COM技术内幕》
等书籍。
COM的分层更高级:
1、它比API更易于兼容扩展,通过包容/聚合增加接口,完美兼容以前程序;
2、利用多态特性实现插件扩展,编程一次,长期复用,接口不变,增加插件就可以扩展功能;
3、利用模板技术,类的类,实现功能复用,比如:STL、ATL、WTL等;

C的分层有以下问题:
1、源码不得不开放给客户,作为商业应用,这没法接受;
2、使用void弱化语法检查,导致
    正确程序调用错误数据
    错误程序调用正确数据
    错误程序调用错误数据
   这个会引起项目失控的危险,出了问题不知道在哪里。
   最好用对象自己处理自己的数据。
3、象链表这样的通用操作其实早就有STL模板可用了,不用自己实现。HASH散列表也有现成的;


象DirectShow等应用就使用COM技术,生成N多filter,想增加一个功能就加一层,比如:增加字幕、增加解码等;
而且filter内部修改不影响外部接口,内部还可以再分层。

调用者最好不要知道数据的操作方法,接口自己知道该怎么做,调用者只负责选择合适的接口。
如果调用者知道数据的操作方法那就是强耦合了。

使用特权

评论回复
评分
参与人数 1威望 +10 收起 理由
hotpower + 10 !!!
55
123jj| | 2011-2-19 08:15 | 只看该作者
NE5532的老师说的一段话实在,顶一把

使用特权

评论回复
56
haishy| | 2011-2-19 15:35 | 只看该作者
学习学习

使用特权

评论回复
57
大先生的梦| | 2011-2-20 21:56 | 只看该作者
看19楼来了 虽然一时理解不了.....

使用特权

评论回复
58
GbWang| | 2011-2-21 22:34 | 只看该作者
现在正在学习中 谢谢周工

使用特权

评论回复
59
john_light| | 2011-2-21 23:56 | 只看该作者
翻页广告位招租,有意者短信联系。;P

使用特权

评论回复
60
hotpower| | 2011-2-25 07:10 | 只看该作者
晕,如此快就下架了???
匠人也太不会经营了,周公这个议题多好,晕

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则