【问题标题】:How to understand this?这个怎么理解?
【发布时间】:2011-04-03 12:34:37
【问题描述】:

来自this question

 gcc -c test.s
 objcopy -O binary test.o test.bin

test.otest.bin 有什么区别?

.text
    call start
    str:
        .string "test\n"
    start:
    movl    $4, %eax
    movl    $1, %ebx
    pop     %ecx
    movl    $5, %edx
    int     $0x80
    ret

上面是做什么的?

【问题讨论】:

标签: assembly objcopy


【解决方案1】:

objcopy -O binary 复制源文件的内容。这里,test.o 是“可重定位目标文件”:即代码,也是符号表和重定位信息,它允许文件与其他文件链接成可执行程序。 objcopy 生成的test.bin 文件只包含代码,没有符号表或重定位信息。这样的“原始”文件对于“普通”编程是无用的,但对于有自己的加载器的代码来说很方便。

我假设您在 32 位 x86 系统上使用 Linux。您的 test.o 文件大小为 515 字节。如果您尝试objdump -x test.o,您会得到以下内容,它描述了test.o 目标文件的内容:

$ objdump -x test.o

test.o:     file format elf32-i386
test.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000001e  00000000  00000000  00000034  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000000  00000000  00000000  00000054  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  00000054  2**2
                  ALLOC
SYMBOL TABLE:
00000000 l    d  .text  00000000 .text
00000000 l    d  .data  00000000 .data
00000000 l    d  .bss   00000000 .bss
0000000b l       .text  00000000 start
00000005 l       .text  00000000 str

这为您提供了很多信息。特别是,该文件包含一个名为.text 的部分,该部分从文件中的偏移量 0x34(十进制的 52)开始,长度为 0x1e 字节(十进制的 30)。您可以将其反汇编以查看操作码本身:

$ objdump -d test.o

test.o:     file format elf32-i386


Disassembly of section .text:

00000000 <str-0x5>:
   0:   e8 06 00 00 00          call   b <start>

00000005 <str>:
   5:   74 65                   je     6c <start+0x61>
   7:   73 74                   jae    7d <start+0x72>
   9:   0a 00                   or     (%eax),%al

0000000b <start>:
   b:   b8 04 00 00 00          mov    $0x4,%eax
  10:   bb 01 00 00 00          mov    $0x1,%ebx
  15:   59                      pop    %ecx
  16:   ba 05 00 00 00          mov    $0x5,%edx
  1b:   cd 80                   int    $0x80
  1d:   c3                      ret    

这或多或少是您开始使用的程序集。中间的jejaeor 操作码是虚假的:这是objdump 试图解释文字字符串("test\n",导致字节0x74 0x65 0x73 0x64 0x0a 0x00)作为操作码。 objdump -d 还显示了在 .text 部分中找到的实际字节,即文件中从偏移量 0x34 开始的字节。第一个字节是 0xe8 0x06 0x00...

现在,看看test.bin 文件。它的长度为 30 个字节。让我们看看那些十六进制的字节:

$ hd test.bin
00000000  e8 06 00 00 00 74 65 73  74 0a 00 b8 04 00 00 00  |.....test.......|
00000010  bb 01 00 00 00 59 ba 05  00 00 00 cd 80 c3        |.....Y........|

我们在这里准确地识别出test.o.text 部分的30 个字节。这就是objcopy -O binary 所做的:它提取了文件内容,即唯一的非空部分,即原始操作码本身,删除了其他所有内容,尤其是符号表和重定位信息。

重定位是关于在给定代码段中必须更改的内容,以便它在存储在内存中的给定位置时正常运行。例如,如果代码使用一个变量并希望获得该变量的地址,那么重定位信息将包含一个条目,告诉将代码实际放入内存的人(通常是链接器) :“在代码中,当你知道变量实际在哪里时,写下变量地址”。有趣的是,您展示的代码不需要重定位:字节序列可以写入任意内存位置并按原样执行。

让我们看看代码做了什么。

  • call 操作码跳转到偏移 0x0b 处的mov 指令。此外,由于这是一个call,它会将返回地址压入堆栈。返回地址是调用完成后应继续执行的位置,即到达ret 操作码时。这是call 操作码后面的字节地址。这里,该地址是文字字符串"test\n" 的第一个字节的地址。
  • 两个movl 分别加载%eax%ebx 的数值4 和1。
  • pop 操作码从堆栈中删除顶部元素,将其存储在 %ecx 中。这个顶级元素是什么?这正是call操作码压入堆栈的地址,即文字字符串的第一个字节的地址。
  • 第三个movl 加载%edx,数值为5。
  • int $0x80 是 32 位 x86 Linux 上的系统调用:它调用内核。内核将查看寄存器以知道该做什么。内核首先查看%eax获取“系统调用号”;在 32 位 x86 上,“4”是__NR_write,即write() 系统调用。此调用需要三个参数,按顺序在寄存器%ebx%ecx%edx 中。这些是目标文件描述符(此处为 1:这是标准输出)、指向要写入的数据的指针(此处为文字字符串)和要写入的数据的长度(此处为 5,对应于四个字母和换行符特点)。所以这会在标准输出上写入"test\n"
  • 最终的ret 返回给调用者。 ret 从堆栈中弹出一个值,然后跳转到该地址。这假定此代码块是使用 call 操作码调用的。

因此,总而言之,代码打印出 test 并带有换行符。

让我们用自定义加载器试试吧:

#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int
main(void)
{
        void *p;
        int f;

        p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        f = open("test.bin", O_RDONLY);
        read(f, p, 30);
        close(f);
        mprotect(p, 30, PROT_READ | PROT_EXEC);
        ((void (*)(void))p)();
        return 0;
}

(上面的代码没有测试返回值是否有错误,这当然很糟糕。)

在这里,我用mmap() 分配了一个内存页(4096 字节),请求一个我可以读写的页。 p 指向那个块。然后,使用open()read()close(),我将test.bin 文件的内容(30 个字节)读入该块。

mprotect() 调用指示内核更改我的页面的访问权限:现在,我希望能够执行这些字节,即将它们视为机器代码。我放弃了写入块的权利(取决于确切的内核配置,可能禁止拥有一个既可以写入又可以执行的页面)。

神秘的((void (*)(void))p)(); 是这样写的:我接受p;我将它转换为指向不带参数且不返回任何函数的函数的指针;我调用那个函数。这是用于将call 放入我的数据块的 C 语法。

当我运行该程序时,我得到:

$ ./blah
test

这是预期的结果:test.bin 中的代码在标准输出上写出 test

【讨论】:

  • 也许它没有安装在你的系统上?在基于 Debian 的 Linux 系统上,它带有“bsdmainutils”软件包。你也可以使用od(来自“coreutils”,在Linux系统上可能比hd更频繁):od -t x1 test.bin
【解决方案2】:

.o 是预链接器,.bin 是后链接器。 这是一篇关于链接器的维基百科文章: http://en.wikipedia.org/wiki/Linker_(computing) 我相信你可以从那里得到重点:)

【讨论】:

  • 那为什么不直接通过gcc -o test.bin test.S生成test.bin呢?
  • 我不使用 gcc,只是回答了你关于 .o 和 .bin 之间差异的问题 :) 那是问题,不是吗?
  • .bin 是任意的;在 Linux 下,它并不意味着任何特定的东西,例如“post-linker”。而objcopy 不是链接器;生成的文件与链接器生成的文件完全不同。
  • 好的。很高兴知道。很抱歉造成混乱。
猜你喜欢
  • 2022-06-15
  • 1970-01-01
  • 1970-01-01
  • 2015-04-07
  • 2018-02-25
  • 2022-07-04
  • 2019-06-13
  • 2011-04-29
  • 1970-01-01
相关资源
最近更新 更多