[翻译_by_gpt] 一个简单的 ELF

URL 来源: https://4zm.org/2024/12/25/a-simple-elf.html

Markdown 内容:
让我们为 Linux 编写一个简单的程序。能有多难呢?嗯,简单是复杂的反义词,而不是困难的反义词,创建一个简单的东西出奇地难。当我们摆脱标准库、所有现代安全功能、调试信息和错误处理机制的复杂性时,还剩下什么?

• • •

让我们从一些复杂的东西开始:

#include <stdio.h>

int main() {
    printf("Hello Simplicity!\n");
}

等等,什么?!看起来并不复杂,对吧……嗯,让我们编译它并看看:

$ gcc -o hello hello.c
$ ./hello
Hello Simplicity!

看起来还是很简单,对吧?错了!虽然这可能是熟悉的领域并且容易理解,但程序远非简单。让我们看看幕后。

$ objdump -t hello

hello:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000              Scrt1.o
000000000000038c l     O .note.ABI-tag  0000000000000020              __abi_tag
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
0000000000001090 l     F .text  0000000000000000              deregister_tm_clones
00000000000010c0 l     F .text  0000000000000000              register_tm_clones
0000000000001100 l     F .text  0000000000000000              __do_global_dtors_aux
0000000000004010 l     O .bss   0000000000000001              completed.0
0000000000003dc0 l     O .fini_array    0000000000000000              __do_global_dtors_aux_fini_array_entry
0000000000001140 l     F .text  0000000000000000              frame_dummy
0000000000003db8 l     O .init_array    0000000000000000              __frame_dummy_init_array_entry
0000000000000000 l    df *ABS*  0000000000000000              hello.c
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
00000000000020f8 l     O .eh_frame  0000000000000000              __FRAME_END__
0000000000000000 l    df *ABS*  0000000000000000
0000000000003dc8 l     O .dynamic   0000000000000000              _DYNAMIC
0000000000002018 l       .eh_frame_hdr  0000000000000000              __GNU_EH_FRAME_HDR
0000000000003fb8 l     O .got   0000000000000000              _GLOBAL_OFFSET_TABLE_
0000000000000000       F *UND*  0000000000000000              __libc_start_main@GLIBC_2.34
0000000000000000  w      *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000004000  w      .data  0000000000000000              data_start
0000000000000000       F *UND*  0000000000000000              puts@GLIBC_2.2.5
0000000000004010 g       .data  0000000000000000              _edata
0000000000001168 g     F .fini  0000000000000000              .hidden _fini
0000000000004000 g       .data  0000000000000000              __data_start
0000000000000000  w      *UND*  0000000000000000              __gmon_start__
0000000000004008 g     O .data  0000000000000000              .hidden __dso_handle
0000000000002000 g     O .rodata    0000000000000004              _IO_stdin_used
0000000000004018 g       .bss   0000000000000000              _end
0000000000001060 g     F .text  0000000000000026              _start
0000000000004010 g       .bss   0000000000000000              __bss_start
0000000000001149 g     F .text  000000000000001e              main
0000000000004010 g     O .data  0000000000000000              .hidden __TMC_END__
0000000000000000  w      *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w    F *UND*  0000000000000000              __cxa_finalize@GLIBC_2.2.5
0000000000001000 g     F .init  0000000000000000              .hidden _init

符号真多!实际上,就符号表而言,这个已经相当简洁了。任何非平凡的程序都会有更多的符号,但即便如此,这些符号都是干什么用的?我们只是打印一个字符串而已!

我们在地址 0x1149.text 段中识别出了我们的 main 函数。但 printf 函数在哪里呢?

事实证明,对于不需要 printf 进行格式化工作的简单情况,GCC 会优化代码并用 libc 中更简单的 puts@GLIBC_2.2.5 替换它。由于符号是未定义的(*UND*),所以地址全是零。当程序与动态库 libc.so 一起加载时,它将被解析。

0000000000001149 g     F .text  000000000000001e              main
0000000000000000       F *UND*  0000000000000000              puts@GLIBC_2.2.5

让我们继续挖掘。程序中有哪些段?我们唯一的数据是硬编码的字符串及其长度。我们肯定只需要一个 .text 段吧?让我们看看我们得到了什么:

$ objdump -h hello

hello:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000318  0000000000000318  00000318  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.gnu.property 00000030  0000000000000338  0000000000000338  00000338  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  0000000000000368  0000000000000368  00000368  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .note.ABI-tag 00000020  000000000000038c  000000000000038c  0000038c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .gnu.hash     00000024  00000000000003b0  00000000000003b0  000003b0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynsym       000000a8  00000000000003d8  00000000000003d8  000003d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .dynstr       0000008d  0000000000000480  0000000000000480  00000480  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version  0000000e  000000000000050e  000000000000050e  0000050e  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .gnu.version_r 00000030  0000000000000520  0000000000000520  00000520  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rela.dyn     000000c0  0000000000000550  0000000000000550  00000550  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .rela.plt     00000018  0000000000000610  0000000000000610  00000610  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 11 .init         0000001b  0000000000001000  0000000000001000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .plt          00000020  0000000000001020  0000000000001020  00001020  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .plt.got      00000010  0000000000001040  0000000000001040  00001040  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .plt.sec      00000010  0000000000001050  0000000000001050  00001050  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .text         00000107  0000000000001060  0000000000001060  00001060  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 16 .fini         0000000d  0000000000001168  0000000000001168  00001168  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 17 .rodata       00000011  0000000000002000  0000000000002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .eh_frame_hdr 00000034  0000000000002014  0000000000002014  00002014  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 19 .eh_frame     000000ac  0000000000002048  0000000000002048  00002048  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 20 .init_array   00000008  0000000000003db8  0000000000003db8  00002db8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .fini_array   00000008  0000000000003dc0  0000000000003dc0  00002dc0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .dynamic      000001f0  0000000000003dc8  0000000000003dc8  00002dc8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 .got          00000048  0000000000003fb8  0000000000003fb8  00002fb8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 24 .data         00000010  0000000000004000  0000000000004000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          00000008  0000000000004010  0000000000004010  00003010  2**0
                  ALLOC
 26 .comment      0000002b  0000000000000000  0000000000000000  00003010  2**0
                  CONTENTS, READONLY

好的,这确实很复杂。不仅仅是一个简单的 .text 段。还有很多段。

这太多了,现在无法处理。程序到底从哪里开始?它从 main 开始,对吧?又错了!

$ objdump -f hello

hello:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x0000000000001060

“起始地址”(也称为入口点)是 _start,而不是 main。这个神秘的函数在 0x1060 必须以某种方式调用我们的 main,但它来自哪里呢?

0000000000001060 g     F .text  0000000000000026              _start

让我们开始简化程序。当我们剥离复杂性时,我们将有机会一次理解一些东西。

没有 libc 的生活

我们程序中的一个主要复杂性来源是标准库。它们用于打印字符串和初始化程序。让我们摆脱它们。

很简单,只需使用:-nostdlib 编译。

不幸的是,这意味着我们不再能访问 printf(或 puts)函数。这很不幸,因为我们仍然想打印“Hello Simplicity!”。

这也意味着我们将失去 _start 函数。它由 C 运行时库(CRT)提供,用于执行一些初始化(如清除 .bss 段)并调用我们的 main 函数。由于我们仍然需要执行我们的 main,我们必须对此做些处理。

幸运的是,我们可以使用 -Wl,-e,<function_name> 提供我们自己的入口点。我们可以直接指定 main 作为我们的入口点,但这意味着将其视为 void main() 而不是 int main()。入口点不返回任何东西。我觉得改变 main 的签名有点过分了;让我们创建自己的 void startup() 函数来调用 main

为了写入 stdout,我们使用 syscall 汇编指令。这条指令是我们请求 Linux 内核为我们做事的方式。在这种情况下,我们希望执行 write 系统调用将字符串写入 stdout(文件描述符 = 1)。稍后,我们还希望调用 exit 以终止进程。

调用 syscall 指令时,我们在 rax 寄存器中传递系统调用号,在 rdirsirdx 寄存器中传递参数。write 系统调用的编号是 0x01exit 系统调用的编号是 0x3c

它们的 C 签名如下:

ssize_t write(int fildes, const void *buf, size_t nbyte);
void exit(int status);

这是我们的新程序 hello-syscall.c

int main() {

  volatile const char message[] = "Hello Simplicity!\n";
  volatile const unsigned long length = sizeof(message) - 1;

  // write(1, message, length)
  asm volatile("mov $1, %%rax\n"                // write syscall number (0x01)
               "mov $1, %%rdi\n"                // Stdout file descriptor (0x01)
               "mov %0, %%rsi\n"                // Message buffer
               "mov %1, %%rdx\n"                // Buffer length
               "syscall"                        // Make the syscall
               :                                // No output operands
               : "r"(message), "r"(length)      // Input operands
               : "%rax", "%rdi", "%rsi", "%rdx" // Clobbered registers
  );

  return 0;
}

void startup() {

  volatile unsigned long status = main();

  // exit(status)
  asm volatile("mov $0x3c, %%rax\n" // exit syscall number (0x3c)
               "mov %0, %%rdi\n"    // exit status
               "syscall"            // Make the syscall
               :                    // No output operands
               : "r"(status)        // Input operands
               : "%rax", "%rdi"     // Clobbered registers
  );
}

如果你在想,volatile 关键字是为了防止 GCC 优化掉变量。而 unsigned long 被用来代替 int 是为了匹配 r__ 64 位寄存器的大小。

我们这样构建它:

gcc -Wl,-entry=startup -nostdlib -o hello-nostd hello-syscall.c

这真的比之前更简单吗?嗯,是的!

除非你习惯了汇编语言、系统调用和自定义入口点,否则它可能不会更容易理解。但简单并不等同于容易。简单是复杂的反义词。复杂的东西本质上很难理解,无论你知道多少。简单的东西只有在你掌握了适当的技能后才会变得容易理解。Rich Hickey 在他 2011 年的演讲 "Simple Made Easy" 中对此进行了精彩的解释。

仍然不相信我们真的让程序变得更简单了吗?让我们看看符号和段:

$ objdump -h -t hello-nostd

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000318  0000000000000318  00000318  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.gnu.property 00000020  0000000000000338  0000000000000338  00000338  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  0000000000000358  0000000000000358  00000358  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     0000001c  0000000000000380  0000000000000380  00000380  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       00000018  00000000000003a0  00000000000003a0  000003a0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       00000001  00000000000003b8  00000000000003b8  000003b8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .text         0000007f  0000000000001000  0000000000001000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  7 .eh_frame_hdr 0000001c  0000000000002000  0000000000002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .eh_frame     00000058  0000000000002020  0000000000002020  00002020  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .dynamic      000000e0  0000000000003f20  0000000000003f20  00002f20  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 10 .comment      0000002b  0000000000000000  0000000000000000  00003000  2**0
                  CONTENTS, READONLY

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 hello-syscall.c
0000000000000000 l    df *ABS*  0000000000000000
0000000000003f20 l     O .dynamic   0000000000000000 _DYNAMIC
0000000000002000 l       .eh_frame_hdr  0000000000000000 __GNU_EH_FRAME_HDR
0000000000001050 g     F .text  000000000000002f startup
0000000000004000 g       .dynamic   0000000000000000 __bss_start
0000000000001000 g     F .text  0000000000000050 main
0000000000004000 g       .dynamic   0000000000000000 _edata
0000000000004000 g       .dynamic   0000000000000000 _end

这里仍然有很多内容,但至少现在它可以在一个屏幕上显示。正如预期的那样,objdump -f 给了我们一个新的起始地址:0x1050。这是我们的 startup 函数!

让我们继续简化!

没有 PIE 的生活

在过去的 20 年里,你的程序被加载到随机地址作为一种安全缓解措施。ASLR(地址空间布局随机化)使得编写漏洞利用变得更加困难,因为 shellcode 不能跳转到硬编码的目标。这也意味着你常规程序中的跳转不能硬编码。

默认情况下,现代系统上的程序被构建为位置无关可执行文件(PIE)。地址在程序加载到内存时解析。这对安全性很有好处,但增加了复杂性。让我们用 -no-pie 去掉它。

为了进一步简化我们的汇编代码,我们用 -fcf-protection=none-fno-stack-protector 关闭一些安全功能。我们还用 -Wl,--build-id=none 去掉一些元数据生成,并用 -fno-unwind-tables-fno-asynchronous-unwind-tables 去掉一些调试器友好的堆栈展开信息。

gcc -no-pie \
    -nostdlib \
    -Wl,-e,startup \
    -Wl,--build-id=none \
    -fcf-protection=none \
    -fno-stack-protector \
    -fno-asynchronous-unwind-tables \
    -fno-unwind-tables \
    -o hello-nostd-nopie hello.c

我们现在得到了这个:

$ objdump -h -t hello-nostd-nopie

hello-nostd-nopie:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000077  0000000000401000  0000000000401000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .comment      0000002b  0000000000000000  0000000000000000  00001077  2**0
                  CONTENTS, READONLY

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 hello-syscall.c
000000000040104c g     F .text  000000000000002b startup
0000000000402000 g       .text  0000000000000000 __bss_start
0000000000401000 g     F .text  000000000000004c main
0000000000402000 g       .text  0000000000000000 _edata
0000000000402000 g       .text  0000000000000000 _end

你注意到使用 -no-pie 后符号地址是如何变化的吗?之前,它们是相对的,等待在加载时添加某个偏移量。现在,它们是绝对的,main 将真正位于 0x00401000

$ gdb hi
(gdb) break main
Breakpoint 1 at 0x401004
(gdb) run
Breakpoint 1, 0x0000000000401004 in main ()

呼!我们终于接近一些简单的东西了。现在,我们的整个程序甚至可以在一个屏幕上显示:

$ objdump -d -M intel hello-nostd-nopie

Disassembly of section .text:

0000000000401000 <main>:
  401000:   55                      push   rbp
  401001:   48 89 e5                mov    rbp,rsp
  401004:   48 b8 48 65 6c 6c 6f    movabs rax,0x6953206f6c6c6548
  40100b:   20 53 69
  40100e:   48 ba 6d 70 6c 69 63    movabs rdx,0x79746963696c706d
  401015:   69 74 79
  401018:   48 89 45 e0             mov    QWORD PTR [rbp-0x20],rax
  40101c:   48 89 55 e8             mov    QWORD PTR [rbp-0x18],rdx
  401020:   66 c7 45 f0 21 0a       mov    WORD PTR [rbp-0x10],0xa21
  401026:   c6 45 f2 00             mov    BYTE PTR [rbp-0xe],0x0
  40102a:   48 c7 45 d8 12 00 00    mov    QWORD PTR [rbp-0x28],0x12
  401031:   00
  401032:   4c 8b 45 d8             mov    r8,QWORD PTR [rbp-0x28]
  401036:   48 8d 4d e0             lea    rcx,[rbp-0x20]
  40103a:   48 c7 c0 01 00 00 00    mov    rax,0x1
  401041:   48 c7 c7 01 00 00 00    mov    rdi,0x1
  401048:   48 89 ce                mov    rsi,rcx
  40104b:   4c 89 c2                mov    rdx,r8
  40104e:   0f 05                   syscall
  401050:   b8 00 00 00 00          mov    eax,0x0
  401055:   5d                      pop    rbp
  401056:   c3                      ret

0000000000401057 <startup>:
  401057:   55                      push   rbp
  401058:   48 89 e5                mov    rbp,rsp
  40105b:   48 83 ec 10             sub    rsp,0x10
  40105f:   b8 00 00 00 00          mov    eax,0x0
  401064:   e8 97 ff ff ff          call   401000 <main>
  401069:   48 98                   cdqe
  40106b:   48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  40106f:   48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
  401073:   48 c7 c0 3c 00 00 00    mov    rax,0x3c
  40107a:   48 89 d7                mov    rdi,rdx
  40107d:   0f 05                   syscall
  40107f:   90                      nop
  401080:   c9                      leave
  401081:   c3                      ret

你可以看到 startup 函数调用 main,两个系统调用,以及硬编码为一大串 ASCII 值的 "Hello Simplicity!" 字符串(相对于栈基指针 rbp 被加载到栈上)。

至少在这个层面上,已经没有太多复杂性了。我们的 ELF 实际上相当简单!但等等,还有更多!

链接脚本

那些奇怪的符号(比如 __bss_start)从哪里来的?是谁决定我们的 startup 函数应该加载到内存中的 0x0040104c?如果我们想让我们的代码位于酷炫的 0xc0d30000 地址范围内怎么办?

这些东西在链接脚本中指定。到目前为止,我们一直在使用默认的链接脚本,你可以通过 ld -verbose 查看它。它非常复杂。让我们摆脱它。

我们的简单 hello world 应用程序不使用任何全局变量。如果有,它们将分为三类:

  • .rodata:编译时提供值的常量,比如我们的硬编码字符串。
  • .data:编译时提供值的非 const 变量。
  • .bss:未初始化的全局变量。

让我们通过引入每个类别的符号来稍微复杂化我们的程序。这将提供一个更有趣的链接脚本示例。这里是新程序 hello-data.c

const char message[] = "Hello Simplicity!\n";   // .rodata
unsigned long length = sizeof(message) - 1;     // .data
unsigned long status;                           // .bss

int main() {
  // write(1, message, length)
  asm volatile("mov $1, %%rax\n"                // write syscall number (0x01)
               "mov $1, %%rdi\n"                // Stdout file descriptor (0x01)
               "mov %0, %%rsi\n"                // Message buffer
               "mov %1, %%rdx\n"                // Buffer length
               "syscall"                        // Make the syscall
               :                                // No output operands
               : "r"(message), "r"(length)      // Input operands
               : "%rax", "%rdi", "%rsi", "%rdx" // Clobbered registers
  );

  return 0;
}

void startup() {
  status = main();

  // exit(status)
  asm volatile("mov $0x3c, %%rax\n" // exit syscall number (0x3c)
               "mov %0, %%rdi\n"    // exit status
               "syscall"            // Make the syscall
               :                    // No output operands
               : "r"(status)        // Input operands
               : "%rax", "%rdi"     // Clobbered registers
  );
}

再次查看符号表,不使用自定义链接脚本,我们可以看到 .data.rodata.bss 中的全局变量分别如下:

000000000040102f g     F .text  000000000000002d startup
0000000000403010 g     O .data  0000000000000008 length
0000000000402000 g     O .rodata    000000000000000e message
0000000000401000 g     F .text  000000000000002f main
0000000000403018 g     O .bss   0000000000000008 status

现在,让我们创建一个简单有趣的链接脚本 (hello.ld),带有酷炫的内存映射和表情符号作为段名:

MEMORY {
  IRAM (rx) : ORIGIN = 0xC0DE0000, LENGTH = 0x1000
  RAM  (rw) : ORIGIN = 0xFEED0000, LENGTH = 0x1000
  ROM  (r)  : ORIGIN = 0xDEAD0000, LENGTH = 0x1000
}

SECTIONS
{
  "📜 .text" : {
    *(.text*)
  } > IRAM

  "📦 .data" : {
    *(.data*)
  } > RAM

  "📁 .bss" : {
    *(.bss*)
  } > RAM

  "🧊 .rodata" : {
    *(.rodata*)
  }  > ROM

  /DISCARD/ : { *(.comment) }
}

ENTRY(startup)

我们使用与之前相同的构建选项,但添加 -T hello.ld 以开始使用我们的链接脚本。

这是最终形式的简单程序:

$ objdump -t -h hello-data

hello-data:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 📜 .text    0000005c  00000000c0de0000  00000000c0de0000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 📦 .data    00000008  00000000feed0000  00000000feed0000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
  2 📁 .bss     00000008  00000000feed0008  00000000feed0008  00003008  2**3
                  ALLOC
  3 🧊 .rodata  00000013  00000000dead0000  00000000dead0000  00002000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 hello-data.c
00000000c0de002f g     F 📜 .text    000000000000002d startup
00000000feed0000 g     O 📦 .data    0000000000000008 length
00000000dead0000 g     O 🧊 .rodata  0000000000000013 message
00000000c0de0000 g     F 📜 .text    000000000000002f main
00000000feed0008 g     O 📁 .bss 0000000000000008 status

是不是非常可爱?

我在 github.com/4ZM/elf-shenanigans 上放了一些示例代码,可以重现本文中的示例。

如果你想了解更多关于链接脚本的内容(为什么不呢?!),这是一个出色的技术文档:c_Using_LD

如果你想探索更多关于段名的荒谬事情,请查看我的另一篇文章:ELF Shenanigans

posted @   ffl  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示