Давайте-ка напишем простую программу для 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
, а для exit
— 0х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/
Добавить комментарий