Давайте-ка напишем простую программу для 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 ?
nerudo
Я не готов изменять сигнатуру main(), на зато вставлю asm(" ")...
Неужели нет методов статически скомпоновать libc, оставив только puts() и exit() ?