Давайте-ка напишем простую программу для Linux. Насколько трудной она может быть? Только тут надо учесть, что простота противоположна сложности, но не трудности*, и создать нечто простое на удивление трудно. А что останется, если избавиться от сложности стандартной библиотеки, всех современных средств безопасности, отладочной информации и механизмов обработки ошибок?

*Прим. пер.: в оригинале автор играет со смыслом слов «complex» — «сложный» и «hard» — «трудный», противопоставляя их значениям «simple» — «простой» и «easy» — «лёгкий».

Начнём с чего-нибудь сложного:

#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

Очень много символов! Вообще, если рассуждать масштабами таблиц символов, то эта ещё довольно скромна. Любая нетипичная программа будет содержать намного больше символов, но суть в другом — зачем они все? Мы же просто выводим строку!

В полученном выводе наша функция main обнаруживается в сегменте .text по адресу 0x1149. Но где же функция printf?

Оказывается, что в простых случаях, когда от printf не требуется никакого форматирования, GCC оптимизирует код, заменяя эту функцию на более простую puts@GLIBC_2.2.5 из libc. Её адрес представлен всеми нулями, так как этот символ неопределён (*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 address, он же точка входа) является _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 номер системного вызова, а в регистрах rdi, rsi и rdx — аргументы. Для системного вызова write используется номер 0х01, а для exit0х3с.

Вот их сигнатуры в С:

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 (0x01)
               "mov $1, %%rdi\n"                // Файловый дескриптор stdout (0x01)
               "mov %0, %%rsi\n"                // Буфер сообщений
               "mov %1, %%rdx\n"                // Длина буфера
               "syscall"                        // Выполнение syscall
               :                                // Операндов вывода нет
               : "r"(message), "r"(length)      // Входные операнды
               : "%rax", "%rdi", "%rsi", "%rdx" // Используемые регистры
  );

  return 0;
}

void startup() {

  volatile unsigned long status = main();

  // exit(status)
  asm volatile("mov $0x3c, %%rax\n" // Номер системного вызова exit (0x3c)
               "mov %0, %%rdi\n"    // exit status 
               "syscall"            // Выполнение syscall
               :                    // Операндов вывода нет
               : "r"(status)        // Входные операнды
               : "%rax", "%rdi"     // Используемые регистры
  );
}

Ключевое слово volatile необходимо, чтобы GCC не убирал в ходе оптимизации переменные. А unsigned long используется вместо int для обеспечения соответствия размеру 64-битных регистров r__.

Теперь мы соберём программу так:

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

Стала ли она ощутимо проще? Ещё бы!

Возможно, её не стало легче понимать, если только вы не знаток ассемблера, системных вызовов и кастомных точек входа. Но простота не является синонимом для лёгкости. Простота — это противоположность сложности. Сложные вещи трудны для понимания по своей сути, вне зависимости от объёма ваших знаний. Простые же вещи трудно понимать, только если у вас нет необходимых навыков. Рич Хикки очень изящно объясняет эту идею в своём выступлении «Simple Made Easy» от 2011 года.

Всё ещё сомневаетесь, что мы реально упростили программу? Давайте снова взглянем на символы и разделы:

$ 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 (Address Space Layout Randomization, случайное распределение адресного пространства) затрудняет написание эксплойтов, так как шеллкод не может переходить на жёстко определённые области памяти. Это также означает, что нельзя жёстко прописать переходы в типичных программах.

По умолчанию программы в современных системах собираются в виде позиционно-независимых исполняемых файлов (Position Independent Executables, 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, а также два системных вызова и строку «Hello Simplicity!», жёстко прописанную в виде большого числа значений ASCII (загружаемых в стек относительно указателя базы rbp).

Сложности осталось не так много, по крайней мере, не на этом уровне. Собственно, наш ELF уже довольно прост. Но есть ещё кое-что…

▍ Скрипты компоновщика


Откуда берутся странные символы (вроде _bss_start)? И кто решает, что наша функция startup должна загружаться в память по адресу 0x0040104c? А если мы хотим, чтобы наш код жил в козырном диапазоне 0xc0d30000?

Всё это определяется в скрипте компоновщика. Пока что мы использовали предустановленный, который можно просмотреть с помощью ld -verbose. Он очень сложный. Нужно от него избавиться.

В нашем простом приложении «Hello world» никакие глобальные переменные не используются. А если бы использовались, то подразделялись бы на три категории:

  • .rodata: константы со значениями, предоставляемыми на этапе компиляции, подобно нашей жёстко прописанной строке.
  • .data: непостоянные переменные, значения которых предоставляются на этапе компиляции.
  • .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 (0x01)
               "mov $1, %%rdi\n"                // Файловый дескриптор stdout (0x01)
               "mov %0, %%rsi\n"                // Буфер сообщений
               "mov %1, %%rdx\n"                // Длина буфера
               "syscall"                        // Выполнение syscall
               :                                // Операндов вывода нет
               : "r"(message), "r"(length)      // Входные операнды
               : "%rax", "%rdi", "%rsi", "%rdx" // Используемые регистры
  );

  return 0;
}

void startup() {
  status = main();

  // exit(status)
  asm volatile("mov $0x3c, %%rax\n" // Номер системного вызова exit (0x3c)
               "mov %0, %%rdi\n"    // exit status  
               "syscall"            // Выполнение syscall
               :                    // Операндов вывода нет
               : "r"(status)        // Входные операнды
               : "%rax", "%rdi"     // Используемые регистры
  );
}

Если взглянуть на таблицу символов теперь, когда в ней нет скрипта компоновщика, мы увидим глобальные переменные в .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.d) с крутой картой памяти и эмодзи в именах разделов:

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, чтобы вы могли воспроизвести примеры из этой статьи.

Для тех, кто захочет подробнее познакомиться со скриптами компоновщика, рекомендую эту прекрасную техническую документацию: «c_Using_LD».

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (1)


  1. nerudo
    12.01.2025 09:10

    Я не готов изменять сигнатуру main(), на зато вставлю asm(" ")...

    Неужели нет методов статически скомпоновать libc, оставив только puts() и exit() ?