zoukankan      html  css  js  c++  java
  • 用简单C程序分析DOS下的EXE文件【转】

    http://blog.chinaunix.net/u3/104230/showart_2082499.html

    用简单C程序分析DOS下的EXE文件

    DOS下的EXE文件格式比较简单,所以咱们先把Windows下的那个复杂的EXE文件放一边,挑个软柿子捏捏(以下EXE如不特殊说明均指DOS下的EXE文件格式)。

    其实网上关于EXE格式的说明很多,大都是哗啦列出大批格式说明,看得人是头晕脑胀的。等自己搞懂了,总觉的其中个别说明不太精确导致自己误解浪费了不少时间。所以,咱们要自己动手去实践一下,边动手边理解就容易多了。至于本次分析为什么用C,这个嘛,咱们随便分析一下C语言与汇编的联系,尤其是子程序的调用,这跟什么什么标准有关。好,废话少说,切入正题!

    1.软件准备

    ①既然跟DOS有关,得有个DOS系统吧。现在盛行虚拟机,安装简单并且系统崩溃的话跟自己电脑的硬件没关联,安全方便。至于怎么安装,请参阅本人拙文:

    VMware上安装MS-DOS 6.22之一:基本系统的安装

    VMware上安装MS-DOS 6.22之二:光驱驱动及其他的安装

    ②另外还要安装上Turbo C。初学C的大概都用过这个东东吧。安装在DOS上吧。我用的是Turbo C++ 3。

    ③还得有个能查看文件16进制的软件。例如UltraEdit。

    2.生成EXE文件

    我们要从最简单的分析起,所以C程序尽量简单,只包含一个子程序调用。如下

    int sum(int x,int y)
    {
        return x+y;
    }

    main()
    {
        int x,y,s;
        x=1;
        y=2;
        s=sum(x,y);
    }

    怎么样,够简单的吧。编译链接生成EXE文件。

    3.小样,来吧,让我分析分析你

    ①用UltraEdit打开生成的EXE文件如下图(只截取了开头一部分)

    下面着重说几个重要的偏移。

    ②上图的被红框圈的两个字母看见了没,这个就是EXE文件的标示,它占用了文件最开头的俩字节。可能你纳闷了为什么用MZ呢?哈哈,自己上网查查吧。

    ③再往下看,在偏移02-03h的地方存放了000C(不要告诉我存放的是0C00),在偏移04-04h的地方存放了0009(请不要替我纠“错”)。通过这两个数据可以计算出文件大小,在这里0009指出该文件用了9个块(1个块是512B),000C指出最后一个块(第9个块)没有用完只用了000C个字节。明白了吧。来,实际计算一下(9﹣1)×512B=4096,再加上12(000Ch)等于4108B。跟DOS里显示的一样。

    ④偏移06-07h处为重定向项目的个数。什么叫重定向呢,简单的说就是:EXE文件必须要加载到内存中才能执行,但是文件中数据的偏移地址跟内存中偏移是不一样的,重定向就是达到重新修改偏移的目的。可以看出我们这个文件中重定向项的个数为1。在这里我们也应该看一下偏移18-19h处的数据,它指出了第一个重定向项目在本文件中的偏移,在本文件中为003Eh。即,在本文件003Eh偏移处存放了第一个重定向项目的内容,它的结构体声明为:

    struct EXE_RELOC
    {
      unsigned short offset;
      unsigned short segment;
    };

    ⑤偏移08-09h处:该处数据指出了EXE头部大小,一般EXE头部后面紧跟着的就是程序数据了。本文件中为0020h,注意它的单位是节,一个节为16个字节,也即程序数据开始于文件偏移200h处。

    ⑥偏移0A-0Bh处:该处数据指出了运行该程序所需的最小内存,如果小于这个内存,程序将不会被加载执行。

    ⑦偏移0C-0Dh处:该处数据指出了运行该程序所需的最大内存,一般为FFFFh。

    ⑧偏移0E-0Fh处:堆栈段在装入模块中的偏移,本文件中为:00E5h
     偏移10-11h处:SP初始值,本文件中为:0080h
     即SS:SP=00E5:0080

    ⑨偏移14-15h处:IP初始值,本文件中为:0
     偏移16-17h处:CS在装入模块中的偏移,本文件中为:0

     我们看看实际加载到内存中是SS、SP和CS、IP是如何分配的

    在DOS下DEBUG一下该EXE文件。即,输入DEBUG CASM.EXE(CASM是我的EXE文件名),用R看一下寄存器。如下图

    分析:DS、ES均指向了PSP段地址,也即PSP段地址为27F2h。那么CS=2802是如何算出来的呢?我们知道PSP长度为100h,如果换算成段地址的话则为10h,那么CS=PSP段地址+10h+CS在装入模块中的偏移=27F2h+10h+0=2802h。IP与文件中初始值相同。我们再来看看堆栈段,SS=PSP段地址+10h+堆栈段在装入模块中的偏移=27F2h+10h+00E5h=28E7,另外SP的值也为初始值相同。

    4.汇编与C

    以上我们分析了EXE头文件的格式,下面我们简单的看一下C语言是如何编译而形成机器码的。

    我们可以看到生成的EXE文件相当庞大,足足有4KB。其实我们才写了几行代码,为什么会生成这么多的代码数据呢?这是因为编译器在链接的时候加上了默认的库,这个就是C RunTime Library。我们可以在链接的时候不去装载这个库,具体如下

    现在我们可以用UltraEdit查看一下,是不是减肥了不少。内存装入模块如下图

    用DOS下的Debug反汇编一下,如下图

    从2802:000D到2802:002E是主函数main的汇编实代码。可以看出我们声明的变量都被放在了堆栈中了,图示如下(以下的图中的堆栈向上增长这句话是错误,应该是反向,另外地址是向上增长的,但堆栈是向下增长的,不好意思向各位道歉)


    接着在调用子程序前,我们可以看到有又有参数压入堆栈,这就是为参数传递做准备。


    然后是CALL 0000,这是一个进调用指令,所以只将IP入栈,并且IP是CALL指令的下一条指令的地址,也即0026入栈。

    此时IP被修改为0,转移到子程序中执行代码。在子程序中第一句PUSH BP第二句MOV BP,SP,经过这两条指令后

    然后就是从堆栈中取传递的参数了,

    MOV AX,[BP+04]
    ADD AX,[BP+06]

    这两句正好取走了主函数中传递的参数,并实现了求和,并将和放在了AX寄存器中。然后执行

    POP BP

    恢复BP寄存器的值。后边的RET指令将保存在堆栈中IP的值弹出并把它赋予IP寄存器,这时程序返回主程序执行指令。

    POP CX
    POP CX

    这两条指令是什么意思呢,为了方便分析,我们再把堆栈图画一下,注意SP此时的指向。

    (注意在这里为什么还把SP一下的数据给画出来,这是因为在实际的内存中数据还保存着)

    原来这两句的作用就是释放传递参数所用的内存,明白了吧。

    MOV [BP-06],AX

    这句作用是将求和得到的值赋给变量s。

    自此子程序的调用过程我们就分析完了。从我们的分析中可以看到堆栈发挥了重要的作用,为变量开辟空间,参数传递,保存寄存器值等等,都要用到它。另外C中关于参数的传递有很多标准,我们刚才分析的应该是CDECL标准。更多信息请搜索一下网络,立刻会有你的答案的。

  • 相关阅读:
    Spring IOC实现原理,源码深度剖析!
    MYSQL配置参数优化详解
    MYSQL 索引优化全攻略
    MySQL性能优化实战
    最强MySQL MVCC实现原理
    redis集群代建
    redis分布式锁
    mysql 的语句的执行顺序
    Rockey pushConsumer 和 pullConsumer 的区别
    RockeyMQ消息处理
  • 原文地址:https://www.cnblogs.com/feng801/p/1610494.html
Copyright © 2011-2022 走看看