• 简单学习看机器码的方法


      我们知道,用C、C++、Java等高级编程语言写的程序,最终都要经过编译链接成本机可执行的程序。这个可执行程序究竟是什么呢?

      在Linux上,我们可以用objdump命令很方便地查看一个可执行程序的机器码。

      好,现在从一个简单的示例开始,说一说怎么理解机器码。

      我们编一个简单的c程序,如下:

    #include <stdio.h>
    void f1()
    {
            int i;
            for(i = 0; i < 10; i++)
            {
                    printf("%d\n", i);
            }
    }
    
    int main()
    {
            printf("start\n");
            f1();
            printf("end\n");
            return 0;
    }
    

      Makefile的内容如下:

    all : test.c
            gcc -o test test.c
            gcc -S test.c
            objdump -D test > dumpresult.txt
    clean :
            rm test test.s dumpresult.txt
    

      程序很简单,我们就不去关心运行结果了。

      首先看生成的test.s,里面main的汇编代码为:

    main:
            pushl   %ebp
            movl    %esp, %ebp
            andl    $-16, %esp
            subl    $16, %esp
            movl    $.LC1, (%esp)
            call    puts
            call    f1
            movl    $.LC2, (%esp)
            call    puts
            movl    $0, %eax
            leave
            ret
    

      函数f1的汇编代码为:

    f1:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $40, %esp
            movl    $0, -12(%ebp)
            jmp     .L2
    .L3:
            movl    $.LC0, %eax
            movl    -12(%ebp), %edx
            movl    %edx, 4(%esp)
            movl    %eax, (%esp)
            call    printf
            addl    $1, -12(%ebp)
    .L2:
            cmpl    $9, -12(%ebp)
            jle     .L3
            leave
            ret
    

      其实作者在写本文的时候,汇编方面相关的基础素养也不高,免强看得懂啦。看这汇编代码确实让人晕得很哈,不像C程序代码那样接近人的思维。

      这里的汇编代码是AT&T语法的,跟部分学校里面开设的汇编课程中所采用的intel语法是不一样的。以intel语法中有mov指令为例,它在AT&T中可能对应movl,而且操作数的方法不一样,intel的是第一个操作数是目的操作数,第二个是源操作数,而AT&T的刚好相反。这里就简单提一点,有兴趣的请谷歌找详细内容。

      这里看过汇编代码之后,再下层就是机器码了,让我们一步一步揭开其真实面纱。

      在Makefile中,我们通过objdump命令将生成的可执行程序进行了反汇编,生成的结果在dumpresult.txt文件中。我们在这个文件中找到咱们main函数,如下:

    08048423 <main>:
     8048423:       55                      push   %ebp
     8048424:       89 e5                   mov    %esp,%ebp
     8048426:       83 e4 f0                and    $0xfffffff0,%esp
     8048429:       83 ec 10                sub    $0x10,%esp
     804842c:       c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
     8048433:       e8 ec fe ff ff          call   8048324 <puts@plt>
     8048438:       e8 b7 ff ff ff          call   80483f4 <f1>
     804843d:       c7 04 24 1a 85 04 08    movl   $0x804851a,(%esp)
     8048444:       e8 db fe ff ff          call   8048324 <puts@plt>
     8048449:       b8 00 00 00 00          mov    $0x0,%eax
     804844e:       c9                      leave
     804844f:       c3                      ret
    

      而f1函数的反汇编如下:

    080483f4 <f1>:
     80483f4:       55                      push   %ebp
     80483f5:       89 e5                   mov    %esp,%ebp
     80483f7:       83 ec 28                sub    $0x28,%esp
     80483fa:       c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%ebp)
     8048401:       eb 18                   jmp    804841b <f1+0x27>
     8048403:       b8 10 85 04 08          mov    $0x8048510,%eax
     8048408:       8b 55 f4                mov    -0xc(%ebp),%edx
     804840b:       89 54 24 04             mov    %edx,0x4(%esp)
     804840f:       89 04 24                mov    %eax,(%esp)
     8048412:       e8 fd fe ff ff          call   8048314 <printf@plt>
     8048417:       83 45 f4 01             addl   $0x1,-0xc(%ebp)
     804841b:       83 7d f4 09             cmpl   $0x9,-0xc(%ebp)
     804841f:       7e e2                   jle    8048403 <f1+0xf>
     8048421:       c9                      leave
     8048422:       c3                      ret
    

      值得说明的是,在test这个可执行程序中,咱们可以用诸如ghex这样的十六进制查看软件进行查看。如在main的起点,它的数据是55 89 e5 83 e4 f0 83 ec 10……这样的数值,如下:

    clip_image001

      这就是机器码,只有机器知道是什么意思,要人来看,估计搞一上午也不定能看懂几行。但咱们的目标是理解它,是要知道为什么f1函数中80483f5行的mov %esp, %ebp是89 e5,而804840f 行的mov %eax,(%esp)却是89 04 24了呢?为什么同样是mov指令,有的以89开头,有的以b8开头,有的以8b开头,等等。

      这得说,这就是人家这样定义的。我在最前面忘了说了,我的电脑CPU是Intel® Celero® E1500的,简单点就是intel32位的。要找到这些机器码为什么是这样的,得从http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html上找人家的手册。请在名为“Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:1, 2A, 2B, 2C, 3A, 3B, and 3C”的这个链接上下载相应的手册,这个上面就说人这些机器码是怎么定义得来的。这个文档有3020页,晕菜了。

      为了快速找到答案,我们直接看第Vol2A 2.1页,讲指令格式的,截图如下:

    clip_image002

      手册上对这样的格式的说明,大意是,一条机器码,Opcode是必须的,其它五个域都是可选的。在本文中所举的例子中大部分都是没有第一个域的,所以这里就不提第一个域了。而第二个域,opcode,是每条指令都有的,它是用哈夫曼算法进行编码的,所以域的长度分为1 2 3字节不等。而这每种编码究竟对应什么指令呢,这个请参考手册上第二卷相关章节的描述。

      以mov指令为例,对mov指令机器码的定义在Vol.2B 4-29页,部分截图如下:

    clip_image003

      可以看出,就光一个mov指令,针对被操作对象的不同也分不同的机器码。所以在本示例情况下,以89开头的机器码,表示Move r32 to r/m32。

      但以mov %esp, %ebp(89 e5)和mov %eax,(%esp)(89 04 24)又是怎么个原理呢?这就得看第三个域ModR/M了,即是对比e5与04的区别。ModR/M分为三个字段,它将一个8比特的字节按2:3:3分开,Mod和R/M域结合着表示指令操作数的寻址方式,Reg部分表示要用到的寄存器。要理解这三个域的意思,得结合Vol.2A 2-5页的表2-2来看了,截图如下:

    clip_image004

      我们将十六进制的e5和04按比特位2:3:3分开,它们所表示的数分别是(11 100 101:3、4、5)和(00 000 100:0、0、4)。对照上表,Mod为11,R/M为101,Reg为100所对应的刚好是E5。而表中E5在r32下所表示的意思是将ESP中的值移到EBP中去。由此反汇编出了mov %esp, %ebp。同理04所表示的意思是将EAX中的值移到某个地方,下面的注释说详情请见SIB域,也就是要在04后面所跟的24上找答案了。

      SIB占一个字节,它所有取值所对应的意思可以在Vol2A 2-6页的表2-3中找到,如下:

    clip_image005

      我们将十六进制的24按2:3:3分开,它所表示的数是(00100100:0、4、4)。对照上表,Scale为00,Index为100,Base为100,刚好对应的值是24,它所表示的意思是数据不做任何处理直接存放在ESP寄存器中。这就反汇编出了mov %eax,(%esp)指令。

      好了,其它的机器码也是按同样的思路去理解。怎么样,有了官方的手册,理解起来容易多了吧。

  • 相关阅读:
    1.01 与 0.99 的法则,Python 实现。
    Python list 数据类型:列表
    CDays3 习题一 (处理命令行参数)及相关内容解析。Python getopt 简介
    Arduino 外部中断
    关于普通定时器与高级定时器的 PWM输出的初始化的区别
    电平配配
    C#就地修改字符串
    将DataTable数据转化为Model对象列表
    SQL Server 事件探查器安装
    C语言模拟泛型粘贴符##的使用 迁移
  • 原文地址:https://www.cnblogs.com/guocai/p/2730048.html
Copyright © 2020-2023  润新知