objcopy -O binary
копирует содержимое исходного файла. Здесь test.o
- это «перемещаемый объектный файл»: это код, а также таблица символов и информация о перемещении, позволяющая связать файл с другими файлами в исполняемой программе. Файл test.bin
, созданный objcopy
, содержит только код, без таблицы символов или информации о перемещении. Такой «сырой» файл бесполезен для «нормального» программирования, но удобен для кода, который имеет собственный загрузчик.
Я предполагаю, что вы используете Linux в 32-битной системе x86. Ваш test.o
файл имеет размер 515 байт. Если вы попробуете objdump -x test.o
, вы получите следующее, описывающее содержимое объектного файла test.o
:
$ objdump -x test.o
test.o: file format elf32-i386
test.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001e 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000054 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000054 2**2
ALLOC
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
0000000b l .text 00000000 start
00000005 l .text 00000000 str
Это дает вам довольно много информации. В частности, файл содержит раздел с именем .text
, начинающийся со смещения 0x34 в файле (это 52 в десятичном виде) и длиной 0x1e байтов (30 в десятичном формате). Вы можете разобрать его, чтобы увидеть сами коды операций:
$ objdump -d test.o
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <str-0x5>:
0: e8 06 00 00 00 call b <start>
00000005 <str>:
5: 74 65 je 6c <start+0x61>
7: 73 74 jae 7d <start+0x72>
9: 0a 00 or (%eax),%al
0000000b <start>:
b: b8 04 00 00 00 mov $0x4,%eax
10: bb 01 00 00 00 mov $0x1,%ebx
15: 59 pop %ecx
16: ba 05 00 00 00 mov $0x5,%edx
1b: cd 80 int $0x80
1d: c3 ret
Это более или менее сборка, с которой вы начали. Коды операций je
, jae
и or
в середине являются ложными: это objdump
пытается интерпретировать буквенную строку ("test\n"
, в результате чего байты 0x74 0x65 0x73 0x64 0x0a 0x00) как коды операций. objdump -d
также показывает фактические байты, найденные в разделе .text
, то есть байты в файле, начинающиеся со смещения 0x34. Первые байты 0xe8 0x06 0x00 ...
Теперь взгляните на файл test.bin
. Имеет длину 30 байт. Давайте посмотрим на эти байты в шестнадцатеричном формате:
$ hd test.bin
00000000 e8 06 00 00 00 74 65 73 74 0a 00 b8 04 00 00 00 |.....test.......|
00000010 bb 01 00 00 00 59 ba 05 00 00 00 cd 80 c3 |.....Y........|
мы распознаем здесь ровно 30 байтов из секции .text
в test.o
. Вот что сделал objcopy -O binary
: он извлек содержимое файла, то есть единственный непустой раздел, то есть сами необработанные коды операций, удалив все остальное, в частности таблицу символов и информацию о перемещении.
Перемещение - это то, что должно быть изменено в данном фрагменте кода, чтобы оно правильно работало при хранении в заданном месте в памяти. Например, если код использует переменную и хочет получить адрес этой переменной, то информация о перемещении будет содержать запись, сообщающую тому, кто фактически поместит код в память (обычно это компоновщик): msgstr "здесь в коде, когда вы знаете, где переменная будет на самом деле, напишите адрес переменной". Интересно, что код, который вы показываете, не нуждается в перемещении: последовательность байтов может быть записана в произвольном месте памяти и выполнена как есть.
Давайте посмотрим, что делает код.
- Код операции
call
переходит к инструкции mov
со смещением 0x0b. Кроме того, поскольку это call
, он помещает в стек адрес возврата. Обратный адрес - это место, где выполнение должно продолжаться после завершения вызова, то есть при достижении кода операции ret
. Это адрес байта, следующего за кодом операции call
. Здесь этот адрес является адресом первого байта литеральной строки "test\n"
.
- Две
movl
нагрузки %eax
и %ebx
с числовыми значениями 4 и 1 соответственно.
- Код операции
pop
удаляет верхний элемент из стека, сохраняя его в %ecx
. Что это за верхний элемент? Именно этот адрес помещается в стек с помощью кода операции call
, то есть адрес первого байта литеральной строки.
- Третий
movl
загружает %edx
с числовым значением 5.
int $0x80
- системный вызов в 32-битном x86 Linux: это вызывает ядро. Ядро будет смотреть на регистры, чтобы знать, что делать. Сначала ядро смотрит на %eax
, чтобы получить «номер системного вызова»; на 32-битном x86 "4" - это __NR_write
, то есть системный вызов write()
. Этот вызов ожидает три параметра в регистрах %ebx
, %ecx
и %edx
в указанном порядке. Это дескриптор файла назначения (здесь 1: это стандартный вывод), указатель на данные для записи (здесь литеральная строка) и длина данных для записи (здесь 5, что соответствует четырем буквам и символу новой строки персонаж). Так что это пишет "test\n"
на стандартный вывод. - Финал
ret
возвращается звонящему.ret
извлекает значение из стека и переходит на этот адрес.Это предполагает, что этот фрагмент кода был вызван с кодом операции call
.
Итак, чтобы подвести итог, код печатает test
с новой строкой.
Давайте попробуем этос пользовательским загрузчиком:
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int
main(void)
{
void *p;
int f;
p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
f = open("test.bin", O_RDONLY);
read(f, p, 30);
close(f);
mprotect(p, 30, PROT_READ | PROT_EXEC);
((void (*)(void))p)();
return 0;
}
(Приведенный выше код не проверяет возвращаемые значения на ошибки, что очень плохо, конечно.)
Здесь я выделяю страницу памяти (4096 байт) с mmap()
, запрашивая страницу, на которой я могу читать и писать.p
указывает на этот кусок.Затем с помощью open()
, read()
и close()
я считываю содержимое файла test.bin
(30 байт) в этот фрагмент.
Вызов mprotect()
дает ядру команду изменитьправа доступа к моей странице: сейчас я хочу иметь возможность выполнять эти байты, т.е. рассматривать их как машинный код.Я отказываюсь от права записи в чанк (в зависимости от точной конфигурации ядра наличие страницы, которая может быть как записана, так и исполнена, может быть запрещено).
Зашифрованный ((void (*)(void))p)();
читается так: Iвзять p
;Я приведу его как указатель на функцию, которая не принимает аргументов и ничего не возвращает;Я вызываю эту функцию.Это синтаксис C для создания call
в моем фрагменте данных.
Когда я запускаю эту программу, я получаю:
$ ./blah
test
, что и ожидалось: код в test.bin
записывает test
на стандартный вывод.