如何使用十六进制编辑器在Linux上创建可执行的ELF文件?

21

只是好奇。显然这不是实际编程的非常好的解决方案,但是假设我想在Bless(一个十六进制编辑器)中创建一个可执行文件。

我的架构是x86。我能制作一个非常简单的程序吗? 一个Hello World? 一个无限循环?类似于这个问题,但在Linux下。


我认为在这个问题的顶部答案中建议的链接将有助于回答你的问题 - https://dev59.com/EXVD5IYBdhLWcg3wVKEb - 提供的简单Linux程序是用来引导一个简单的编译器。 - Brandin
最简单的方法是通过打印“Hello World”来实现,显然。首先在C语言中打印“Hello World”,然后使用gcc -S编译并检查编译器生成的汇编代码输出,或者使用objdump -d命令查看可执行文件的操作码。 - user2845360
1
一个很好的资源是Muppetlabs-TinyPrograms。在Linux上,你将基本上会为可执行文件编写自己的 ELF header,消除不需要的部分。这可能是我发现的最好的参考资料。 - David C. Rankin
最简单的?重新实现truefalse - ninjalj
2个回答

59

反编译NASM hello world并理解其中的每个字节

这个答案的版本有一个漂亮的目录和更多的内容:http://www.cirosantilli.com/elf-hello-world(在这里达到了30k字符的限制)

标准

ELF由LSB指定:

LSB基本上链接到其他标准,具有较小的扩展,特别是:

可以在以下链接找到方便的摘要:

man elf

通过像readelfobjdump这样的工具,可以以人类可读的方式检查其结构。

生成示例

让我们来分解一个最简单可运行的Linux x86-64示例:

section .data
    hello_world db "Hello world!", 10
    hello_world_len  equ $ - hello_world
section .text
    global _start
    _start:
        mov rax, 1
        mov rdi, 1
        mov rsi, hello_world
        mov rdx, hello_world_len
        syscall
        mov rax, 60
        mov rdi, 0
        syscall

编译器版本:

nasm -w+all -f elf64 -o 'hello_world.o' 'hello_world.asm'
ld -o 'hello_world.out' 'hello_world.o'

版本:
  • NASM 2.10.09
  • Binutils 版本 2.24(包含ld
  • Ubuntu 14.04

我们不使用 C 程序,因为那会使分析变得复杂,那将是第二级 :-)

十六进制转储

hd hello_world.o
hd hello_world.out

输出在:https://gist.github.com/cirosantilli/7b03f6df2d404c0862c6

全局文件结构

ELF 文件包含以下部分:

  • ELF 头。指向节头表和程序头表的位置。

  • 节头表(可选的可执行文件)。每个都有 e_shnum 节头,每个都指向一个节的位置。

  • N 个节,其中 N <= e_shnum(可选的可执行文件)

  • 程序头表(仅限可执行文件)。每个都有 e_phnum 程序头,每个都指向一个段的位置。

  • N 个段,其中 N <= e_phnum(可选的可执行文件)

这些部分的顺序是不固定的:唯一固定的是 ELF 头必须是文件上的第一件事情:通用文档说:

ELF头文件

观察头文件最简单的方法是:

readelf -h hello_world.o
readelf -h hello_world.out

输出位置:https://gist.github.com/cirosantilli/7b03f6df2d404c0862c6

目标文件中的字节:

00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 07 00 03 00  |....@.....@.....|

可执行文件:

00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  b0 00 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  10 01 00 00 00 00 00 00  |@...............|
00000030  00 00 00 00 40 00 38 00  02 00 40 00 06 00 03 00  |....@.8...@.....|

结构表示:

typedef struct {
    unsigned char   e_ident[EI_NIDENT];
    Elf64_Half      e_type;
    Elf64_Half      e_machine;
    Elf64_Word      e_version;
    Elf64_Addr      e_entry;
    Elf64_Off       e_phoff;
    Elf64_Off       e_shoff;
    Elf64_Word      e_flags;
    Elf64_Half      e_ehsize;
    Elf64_Half      e_phentsize;
    Elf64_Half      e_phnum;
    Elf64_Half      e_shentsize;
    Elf64_Half      e_shnum;
    Elf64_Half      e_shstrndx;
} Elf64_Ehdr;

手动分解:

  • 0 0: EI_MAG = 7f 45 4c 46 = 0x7f 'E', 'L', 'F': ELF 魔数

  • 0 4: EI_CLASS = 02 = ELFCLASS64: 64 位 elf

  • 0 5: EI_DATA = 01 = ELFDATA2LSB: 大端数据

  • 0 6: EI_VERSION = 01: 格式版本

  • 0 7: EI_OSABI (只在 2003 版本更新中) = 00 = ELFOSABI_NONE: 没有扩展。

  • 0 8: EI_PAD = 8x 00: 保留字节,必须设置为 0。

  • 1 0: e_type = 01 00 = 1 (大端) = ET_REl: 可重定位格式

    可执行文件时为 02 00 表示 ET_EXEC

  • 1 2: e_machine = 3e 00 = 62 = EM_X86_64: AMD64 架构

  • 1 4: e_version = 01 00 00 00: 必须为 1

  • 1 8: e_entry = 8x 00: 执行地址入口点,如果不适用(如对象文件)则为 0。

    可执行文件中为 b0 00 40 00 00 00 00 00。TODO:还可以设置什么?内核似乎直接将 IP 放在该值上,没有硬编码。

  • 2 0: e_phoff = 8x 00: 程序头表偏移量,如果不存在则为 0。

    可执行文件中为 40 00 00 00,即它紧跟在 ELF 头之后。

  • 2 8: e_shoff = 40 7x 00 = 0x40: 节头表文件偏移量,如果不存在则为 0。

  • 3 0: e_flags = 00 00 00 00 TODO。架构特定。

  • 3 4: e_ehsize = 40 00: ELF 头的大小。TODO 为什么有这个字段?它如何变化?

  • 3 6: e_phentsize = 00 00: 每个程序头的大小,如果不存在则为 0。

    可执行文件中为 38 00,它有 56 个字节长

  • 3 8: e_phnum = 00 00: 程序头的条目数,如果不存在则为 0。

    可执行文件中为 02 00,共有 2 个条目。

  • 3 A

    节头表

    Elf64_Shdr结构体的数组。

    每个条目包含有关给定节的元数据。

    ELF头文件中的e_shoff给出了起始位置,这里是0x40。

    ELF头文件中的e_shentsizee_shnum表示我们有7个条目,每个条目长0x40字节。

    因此,该表从0x40到0x40 + 7 + 0x40 - 1 = 0x1FF占用字节。

    某些节名称保留用于特定的节类型:http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#special_sections例如,.text需要一个SHT_PROGBITS类型和SHF_ALLOC + SHF_EXECINSTR

    readelf -S hello_world.o

    There are 7 section headers, starting at offset 0x40:
    
    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
           0000000000000000  0000000000000000           0     0     0
      [ 1] .data             PROGBITS         0000000000000000  00000200
           000000000000000d  0000000000000000  WA       0     0     4
      [ 2] .text             PROGBITS         0000000000000000  00000210
           0000000000000027  0000000000000000  AX       0     0     16
      [ 3] .shstrtab         STRTAB           0000000000000000  00000240
           0000000000000032  0000000000000000           0     0     1
      [ 4] .symtab           SYMTAB           0000000000000000  00000280
           00000000000000a8  0000000000000018           5     6     4
      [ 5] .strtab           STRTAB           0000000000000000  00000330
           0000000000000034  0000000000000000           0     0     1
      [ 6] .rela.text        RELA             0000000000000000  00000370
           0000000000000018  0000000000000018           4     2     4
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
      I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)
    

    每个条目由 struct 表示:
    typedef struct {
        Elf64_Word  sh_name;
        Elf64_Word  sh_type;
        Elf64_Xword sh_flags;
        Elf64_Addr  sh_addr;
        Elf64_Off   sh_offset;
        Elf64_Xword sh_size;
        Elf64_Word  sh_link;
        Elf64_Word  sh_info;
        Elf64_Xword sh_addralign;
        Elf64_Xword sh_entsize;
    } Elf64_Shdr;
    

    章节

    索引0部分

    包含在0x40到0x7F字节中。

    第一部分总是神奇的: http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html说:

    如果节的数量大于或等于SHN_LORESERVE(0xff00),e_shnum的值为SHN_UNDEF(0),实际节头表条目的数量包含在索引0处的节头的sh_size字段中(否则,初始条目的sh_size成员包含0)。

    还有其他魔法部分详细说明在图4-7:特殊部分索引中。


    SHT_NULL

    在索引0中,SHT_NULL是强制性的。它还有其他用途吗: What is the use of the SHT_NULL section in ELF??

    .数据段

    .data 是第一节:

    00000080  01 00 00 00 01 00 00 00  03 00 00 00 00 00 00 00  |................|
    00000090  00 00 00 00 00 00 00 00  00 02 00 00 00 00 00 00  |................|
    000000a0  0d 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
    000000b0  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
    
    • 80 0: sh_name = 01 00 00 00: 在.shstrtab字符串表中的索引1处

      这里的1表示该节的名称从该节的第一个字符开始,直到第一个NUL字符结束,组成字符串.data

      .data是具有预定义含义的节名之一http://www.sco.com/developers/gabi/2003-12-17/ch4.strtab.html

      这些节包含对程序内存映像有贡献的已初始化数据。

    • 80 4: sh_type = 01 00 00 00: SHT_PROGBITS:节内容由ELF未指定,仅由程序如何解释它来确定。对于.data节来说很正常。

    • 80 8: sh_flags = 03 7x 00SHF_ALLOCSHF_EXECINSTRhttp://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#sh_flags,符合.data节的要求

    • 90 0: sh_addr = 8x 00:该节在执行期间将被放置在哪个虚拟地址中,如果未放置则为0

    • 90 8: sh_offset = 00 02 00 00 00 00 00 00 = 0x200:从程序开始到该节第一个字节的字节数

    • a0 0: sh_size = 0d 00 00 00 00 00 00 00

      如果我们从偏移量200处取0xD个字节,我们会看到:

      00000200  48 65 6c 6c 6f 20 77 6f  72 6c 64 21 0a 00        |Hello world!..  |
      

      AHA!所以我们的"Hello world!"字符串就在数据节中,就像我们告诉NASM的那样。

      一旦我们毕业了,我们会这样查找:

      readelf -x .data hello_world.o
      

      输出如下:

      Hex dump of section '.data':
        0x00000000 48656c6c 6f20776f 726c6421 0a       Hello world!.
      

      NASM为该节设置了不错的属性,因为它会神奇地处理.datahttp://www.nasm.us/doc/nasmdoc7.html#section-7.9.2

      还要注意,这是一个糟糕的节选择:好的C编译器会将字符串放在.rodata中,因为它是只读的,并且可以允许进一步的操作系统优化。

    • a0 8: sh_linksh_info = 8x 0:不适用于此节类型。http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#special_sections

    • b0 0: sh_addralign = 04 = TODO:为什么需要

      文本部分

      现在我们已经手动完成了一个部分,让我们升级并使用其他部分的readelf -S

        [Nr] Name              Type             Address           Offset
             Size              EntSize          Flags  Link  Info  Align
        [ 2] .text             PROGBITS         0000000000000000  00000210
             0000000000000027  0000000000000000  AX       0     0     16
      

      .text 是可执行的,但不可写入:如果我们尝试向其中写入内容,Linux 将会崩溃。让我们看看是否真的有一些代码在里面:

      objdump -d hello_world.o
      

      给出:

      hello_world.o:     file format elf64-x86-64
      
      
      Disassembly of section .text:
      
      0000000000000000 <_start>:
         0:       b8 01 00 00 00          mov    $0x1,%eax
         5:       bf 01 00 00 00          mov    $0x1,%edi
         a:       48 be 00 00 00 00 00    movabs $0x0,%rsi
        11:       00 00 00
        14:       ba 0d 00 00 00          mov    $0xd,%edx
        19:       0f 05                   syscall
        1b:       b8 3c 00 00 00          mov    $0x3c,%eax
        20:       bf 00 00 00 00          mov    $0x0,%edi
        25:       0f 05                   syscall
      

      如果我们在hd上grep b8 01 00 00,我们会发现这只出现在00000210,这也是该部分所说的内容。大小为27,这也匹配。因此我们一定在谈论正确的部分。
      这看起来像是正确的代码:一个write,然后是exit。
      最有趣的部分是a行,它执行以下操作:
      movabs $0x0,%rsi
      

      将字符串地址传递给系统调用。目前,0x0只是一个占位符。在链接完成后,它将被修改为包含:

      4000ba: 48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
      

      这个修改是由于.rela.text部分的数据而可能的。

      SHT_STRTAB

      sh_type == SHT_STRTAB 的部分称为字符串表

      它们保存了一个以 null 分隔的字符串数组。

      当需要使用字符串名称时,其他部分会使用这些部分。使用部分会说:

      • 他们使用哪个字符串表
      • 目标字符串表中字符串开始的索引是多少

      例如,我们可以有一个包含以下内容的字符串表: TODO: 它必须以\0开头吗?

      Data: \0 a b c \0 d e f \0
      Index: 0 1 2 3  4 5 6 7  8
      

      如果另一个部分想要使用字符串d e f,他们必须指向该部分的索引5(字母d)。

      值得注意的字符串表部分:

      • .shstrtab
      • .strtab

      .shstrtab

      部分类型:sh_type == SHT_STRTAB

      通用名称:节头字符串表

      节名.shstrtab是保留的。标准规定:

      此节保存节名称。

      该节由ELF头文件中的e_shstrnd字段指向。

      该节的字符串索引由节头的sh_name字段指向,这些字段表示字符串。

      该节没有标记为SHF_ALLOC,因此不会出现在执行程序中。

      readelf -x .shstrtab hello_world.o
      

      给出:

      Hex dump of section '.shstrtab':
        0x00000000 002e6461 7461002e 74657874 002e7368 ..data..text..sh
        0x00000010 73747274 6162002e 73796d74 6162002e strtab..symtab..
        0x00000020 73747274 6162002e 72656c61 2e746578 strtab..rela.tex
        0x00000030 7400                                t.
      

      本节数据具有固定格式:http://www.sco.com/developers/gabi/2003-12-17/ch4.strtab.html

      如果我们看一下其他节的名称,我们会发现它们都包含数字,例如.text节是数字7

      然后每个字符串在找到第一个NUL字符时结束,例如字符12是在.text\0之后的\0

      .symtab

      节类型:sh_type == SHT_SYMTAB

      通用名称:符号表

      首先我们注意到:

      • sh_link = 5
      • sh_info = 6

      对于SHT_SYMTAB节,这些数字意味着:

      • 给出符号名称的字符串在第5节中,.strtab
      • 重定位数据在第6节中,.rela.text

      一个很好的高级工具来反汇编该部分是:

      nm hello_world.o
      

      这将会给出:

      0000000000000000 T _start
      0000000000000000 d hello_world
      000000000000000d a hello_world_len
      

      这只是一个高层次的视图,省略了一些类型的符号和符号类型。可以通过以下方式获得更详细的反汇编:

      readelf -s hello_world.o
      

      这将会给出:

      Symbol table '.symtab' contains 7 entries:
         Num:    Value          Size Type    Bind   Vis      Ndx Name
           0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
           1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello_world.asm
           2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
           3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
           4: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 hello_world
           5: 000000000000000d     0 NOTYPE  LOCAL  DEFAULT  ABS hello_world_len
           6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    2 _start
      

      该表的二进制格式在http://www.sco.com/developers/gabi/2003-12-17/ch4.symtab.html中有记录。

      数据为:

      readelf -x .symtab hello_world.o
      

      这将会给出:

      Hex dump of section '.symtab':
        0x00000000 00000000 00000000 00000000 00000000 ................
        0x00000010 00000000 00000000 01000000 0400f1ff ................
        0x00000020 00000000 00000000 00000000 00000000 ................
        0x00000030 00000000 03000100 00000000 00000000 ................
        0x00000040 00000000 00000000 00000000 03000200 ................
        0x00000050 00000000 00000000 00000000 00000000 ................
        0x00000060 11000000 00000100 00000000 00000000 ................
        0x00000070 00000000 00000000 1d000000 0000f1ff ................
        0x00000080 0d000000 00000000 00000000 00000000 ................
        0x00000090 2d000000 10000200 00000000 00000000 -...............
        0x000000a0 00000000 00000000                   ........
      

      这些条目的类型为:

      typedef struct {
          Elf64_Word  st_name;
          unsigned char   st_info;
          unsigned char   st_other;
          Elf64_Half  st_shndx;
          Elf64_Addr  st_value;
          Elf64_Xword st_size;
      } Elf64_Sym;
      

      就像在节表中一样,第一个条目是神奇的,并设置为固定的无意义值。

      STT_FILE

      条目1具有ELF64_R_TYPE == STT_FILEELF64_R_TYPEst_info内继续。

      字节分析:

      • 10 8: st_name = 01000000 = .strtab 中的第一个字符,直到下一个 \0,即为 hello_world.asm

        这个信息文件可以被链接器用来决定哪些段应该放在哪里。

      • 10 12: st_info = 04

        位 0-3 = ELF64_R_TYPE = 类型 = 4 = STT_FILE:此条目的主要目的是使用 st_name 来指示生成此对象文件的文件名。

        位 4-7 = ELF64_ST_BIND = 绑定 = 0 = STB_LOCAL。对于 STT_FILE,必须使用此值。

      • 10 13: st_shndx = 符号表节头索引 = f1ff = SHN_ABS。对于 STT_FILE,必须使用此值。

      • 20 0: st_value = 8x 00:对于 STT_FILE 必须使用此值

      • 20 8: st_size = 8x 00:未分配大小

      现在从readelf,我们快速解释其他内容。


      STT_SECTION

      有两个这样的条目,一个指向.data,另一个指向.text(部分索引12)。

      Num:    Value          Size Type    Bind   Vis      Ndx Name
        2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
        3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2
      

      待办事项:它们的目的是什么?

      STT_NOTYPE

      接下来是最重要的符号:

      Num:    Value          Size Type    Bind   Vis      Ndx Name
        4: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 hello_world
        5: 000000000000000d     0 NOTYPE  LOCAL  DEFAULT  ABS hello_world_len
        6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    2 _start
      

      hello_world字符串位于.data部分(索引1)。它的值为0:它指向该部分的第一个字节。

      _start由于我们编写了代码,因此标有GLOBAL可见性:

      global _start
      

      在NASM中,这是必要的,因为它必须被视为入口点。与C不同,默认情况下NASM标签是本地的。

      SHN_ABS

      hello_world_len指向特殊的st_shndx == SHN_ABS == 0xF1FF

      选择0xF1FF是为了避免与其他部分冲突。

      st_value == 0xD == 13,这是我们在汇编中存储的字符串Hello World!的长度。

      这意味着重定位不会影响此值:它是一个常量。

      这是我们的汇编器为我们执行的小优化,具有ELF支持。

      如果我们在任何地方使用了hello_world_len的地址,汇编器将无法将其标记为SHN_ABS,链接器稍后将对其进行额外的重定位工作。

      SHT_SYMTAB在可执行文件上

      默认情况下,NASM也会在可执行文件上放置.symtab

      这仅用于调试。没有符号,我们完全是盲目的,必须反向工程一切。

      你可以使用objcopy去除它,可执行文件仍然可以运行。这种可执行文件称为剥离的可执行文件

      .strtab

      保存符号表的字符串。

      此部分具有sh_type == SHT_STRTAB

      它由.symtab部分的sh_link == 5指向。

      readelf -x .strtab hello_world.o
      

      给出:

      Hex dump of section '.strtab':
        0x00000000 0068656c 6c6f5f77 6f726c64 2e61736d .hello_world.asm
        0x00000010 0068656c 6c6f5f77 6f726c64 0068656c .hello_world.hel
        0x00000020 6c6f5f77 6f726c64 5f6c656e 005f7374 lo_world_len._st
        0x00000030 61727400                            art.
      

      这意味着全局变量不能包含NUL字符是ELF级别的限制。

      .rela.text

      节类型:sh_type == SHT_RELA

      通用名称:重定位节

      .rela.text 包含重定位数据,它说明了在最终可执行文件链接时地址应该如何修改。这指向文本区域的字节,在链接发生时必须修改以指向正确的内存位置。

      基本上,它将包含占位符0x0地址的对象文本进行翻译:

         a:       48 be 00 00 00 00 00    movabs $0x0,%rsi
        11:       00 00 00
      

      到包含最终0x6000d8的实际可执行代码:

      4000ba: 48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
      4000c1: 00 00 00
      

      它被 .symtab 节的 sh_info = 6 指向。

      readelf -r hello_world.o 的输出如下:

      Relocation section '.rela.text' at offset 0x3b0 contains 1 entries:
        Offset          Info           Type           Sym. Value    Sym. Name + Addend
      00000000000c  000200000001 R_X86_64_64       0000000000000000 .data + 0
      

      在可执行文件中不存在该部分。

      实际字节为:

      00000370  0c 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|
      00000380  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
      

      所表示的struct是:

      typedef struct {
          Elf64_Addr  r_offset;
          Elf64_Xword r_info;
          Elf64_Sxword    r_addend;
      } Elf64_Rela;
      

      所以:

      • 370 0: r_offset = 0xC:指向将修改此重定位的地址的.text中的地址

      • 370 8: r_info = 0x200000001。包含2个字段:

        • ELF64_R_TYPE = 0x1:具体架构取决于其含义。
        • ELF64_R_SYM = 0x2:地址指向的部分的索引,因此为索引2的.data

        AMD64 ABI表示类型1称为R_X86_64_64,它表示操作S + A,其中:

        • S:对象文件上符号的值,在这里为0,因为我们指向movabs $0x0,%rsi00 00 00 00 00 00 00 00
        • A:附加项,存在于字段r_added

        该地址添加到重定位操作所在的部分。

        此重定位操作作用于总共8个字节。

      • 380 0: r_addend = 0

      在我们的例子中,我们得出新地址将是:S + A = .data + 0,因此数据段中的第一件事。

      程序头表

      仅出现在可执行文件中。

      包含有关如何将可执行文件放入进程虚拟内存的信息。

      可执行文件是由链接器从目标文件生成的。链接器的主要工作包括:

      • 确定目标文件的哪些部分将进入可执行文件的哪些段中。

        在Binutils中,这归结为解析链接器脚本,并处理一堆默认值。

        您可以使用ld --verbose获取使用的链接器脚本,并使用ld -T设置自定义脚本。

      • 对文本部分进行重定位。这取决于多个部分如何放入内存。

      readelf -l hello_world.out给出:

      Elf file type is EXEC (Executable file)
      Entry point 0x4000b0
      There are 2 program headers, starting at offset 64
      
      Program Headers:
        Type           Offset             VirtAddr           PhysAddr
                       FileSiz            MemSiz              Flags  Align
        LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                       0x00000000000000d7 0x00000000000000d7  R E    200000
        LOAD           0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
                       0x000000000000000d 0x000000000000000d  RW     200000
      
       Section to Segment mapping:
        Segment Sections...
         00     .text
         01     .data
      

      在 ELF 头部中,e_phoffe_phnume_phentsize 告诉我们有 2 个程序头,它们从 0x40 开始,每个长 0x38 字节,因此它们是:
      00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................|
      00000050  00 00 40 00 00 00 00 00  00 00 40 00 00 00 00 00  |..@.......@.....|
      00000060  d7 00 00 00 00 00 00 00  d7 00 00 00 00 00 00 00  |................|
      00000070  00 00 20 00 00 00 00 00                           |.. .....        |
      

      并且:

      00000070                           01 00 00 00 06 00 00 00  |        ........|
      00000080  d8 00 00 00 00 00 00 00  d8 00 60 00 00 00 00 00  |..........`.....|
      00000090  d8 00 60 00 00 00 00 00  0d 00 00 00 00 00 00 00  |..`.............|
      000000a0  0d 00 00 00 00 00 00 00  00 00 20 00 00 00 00 00  |.......... .....|
      

      结构体表示 http://www.sco.com/developers/gabi/2003-12-17/ch5.pheader.html

      typedef struct {
          Elf64_Word  p_type;
          Elf64_Word  p_flags;
          Elf64_Off   p_offset;
          Elf64_Addr  p_vaddr;
          Elf64_Addr  p_paddr;
          Elf64_Xword p_filesz;
          Elf64_Xword p_memsz;
          Elf64_Xword p_align;
      } Elf64_Phdr;
      

      第一个的分解:

      • 40 0: p_type = 01 00 00 00 = PT_LOAD: 待办事项。我认为它意味着它将实际加载到内存中。其他类型可能不一定。
      • 40 4: p_flags = 05 00 00 00 = 可执行和读取权限,无写入 TODO
      • 40 8: p_offset = 8x 00 TODO:这是什么?看起来像从段的开头开始的偏移量。但这意味着一些段是交织在一起的吗?可以使用以下命令进行一些调整:gcc -Wl,-Ttext-segment=0x400030 hello_world.c
      • 50 0: p_vaddr = 00 00 40 00 00 00 00 00:将该段加载到的初始虚拟内存地址
      • 50 8: p_paddr = 00 00 40 00 00 00 00 00:要加载到内存中的初始物理地址。仅适用于程序可以设置其物理地址的系统。否则,如System V等系统中一样,可以是任何值。NASM似乎只是复制了p_vaddrr
      • 60 0: p_filesz = d7 00 00 00 00 00 00 00:待办事项与p_memsz相比
      • 60 8: p_memsz = d7 00 00 00 00 00 00 00:待办事项
      • 70 0: p_align = 00 00 20 00 00 00 00 00:0或1表示不需要对齐 TODO这是什么意思?否则与其他字段重复
      第二个类似。
      然后:
       Section to Segment mapping:
      

      readelf 的这一部分告诉我们:

      • 0 是 .text 段。啊哈,所以这就是为什么它是可执行的,而不是可写的。
      • 1 是 .data 段。

@ShankarDamodaran 谢谢!已上传带有目录的改进版到:http://www.cirosantilli.com/elf-hello-world,并在此处更新。 - Ciro Santilli OurBigBook.com
谢谢提供链接。不知道为什么这个答案没有被注意到。我忍不住给它颁发了奖励。工作做得非常出色。 - Shankar Narayana Damodaran
@ShankarDamodaran 谢谢!这可能是因为它是对一个旧问题的新回答。也许标题没有很好地为有机搜索措辞。最重要的是:低级别的问题不会给出太多声望 :-) - Ciro Santilli OurBigBook.com
@CiroSantilli,可以将mov rax, 1这样的指令更改为更短的mov eax, 1,因为它会自动填充rax的上半部分为0(寄存器将被零扩展)。 - anon

15

如我在评论中提到的那样,您将基本上为可执行文件编写自己的elf-header,以消除不必要的部分。仍然有几个必需的部分。在Muppetlabs-TinyPrograms文档中,对这个过程进行了很好的解释。以下是一些示例,供您参考:

/bin/true相当于(45字节):

00000000  7F 45 4C 46 01 00 00 00  00 00 00 00 00 00 49 25  |.ELF..........I%|
00000010  02 00 03 00 1A 00 49 25  1A 00 49 25 04 00 00 00  |......I%..I%....|
00000020  5B 5F F2 AE 40 22 5F FB  CD 80 20 00 01           |[_..@"_... ..|
0000002d

你经典的“Hello World!”(160字节):

00000000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 01 00 00 00  74 80 04 08 34 00 00 00  |........t...4...|
00000020  00 00 00 00 00 00 00 00  34 00 20 00 02 00 28 00  |........4. ...(.|
00000030  00 00 00 00 01 00 00 00  74 00 00 00 74 80 04 08  |........t...t...|
00000040  74 80 04 08 1f 00 00 00  1f 00 00 00 05 00 00 00  |t...............|
00000050  00 10 00 00 01 00 00 00  93 00 00 00 93 90 04 08  |................|
00000060  93 90 04 08 0d 00 00 00  0d 00 00 00 06 00 00 00  |................|
00000070  00 10 00 00 b8 04 00 00  00 bb 01 00 00 00 b9 93  |................|
00000080  90 04 08 ba 0d 00 00 00  cd 80 b8 01 00 00 00 31  |...............1|
00000090  db cd 80 48 65 6c 6c 6f  20 77 6f 72 6c 64 21 0a  |...Hello world!.|
000000a0

别忘了让它们可执行...


相当令人印象深刻。将其与NASM打印“Hello World”所需的0x390字节相比较。虽然不建议给初学者使用 :-) - Ciro Santilli OurBigBook.com

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接