Упрощаем «простой» ELF

от автора


Давайте-ка напишем простую программу для 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 💻


ссылка на оригинал статьи https://habr.com/ru/articles/870674/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *