Итак, давайте go рассмотрим его шаг за шагом.
Начните с этого C файла:
#include <stdio.h>
void printit()
{
puts("Hello world!");
}
int main(void)
{
printit();
return 0;
}
Поскольку комментарии выглядят так, как будто вы находитесь на x86, скомпилируйте его как 32-битный исполняемый файл, отличный от P IE, например:
$ gcc -m32 -no-pie -o test test.c
Требуется опция -m32
, потому что я работаю на машине x86-64. Как вы уже знаете, вы можете получить адрес виртуальной памяти main, используя readelf, objdump или nm, например, так:
$ nm test | grep -w main
0804918d T main
Очевидно, 804918d
не может быть смещением в файле, который всего 15 КБ. Вам нужно найти соответствие между адресами виртуальной памяти и смещениями файлов . В типичном файле ELF отображение включается дважды . Один раз в подробной форме для компоновщиков (поскольку объектные файлы также являются файлами ELF) и отладчиков, а второй раз в сжатой форме, которая используется ядром для загрузки программ. Подробная форма - это список разделов, состоящий из заголовков разделов, и вы можете просмотреть его так (вывод немного сокращен, чтобы сделать ответ более читаемым):
$ readelf --section-headers test
There are 29 section headers, starting at offset 0x3748:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[...]
[11] .init PROGBITS 08049000 001000 000020 00 AX 0 0 4
[12] .plt PROGBITS 08049020 001020 000030 04 AX 0 0 16
[13] .text PROGBITS 08049050 001050 0001c1 00 AX 0 0 16
[14] .fini PROGBITS 08049214 001214 000014 00 AX 0 0 4
[15] .rodata PROGBITS 0804a000 002000 000015 00 A 0 0 4
[...]
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
Здесь вы обнаружите, что Раздел .text
начинается с (виртуального) адреса 08049050
и имеет размер 1c1
байт, поэтому он заканчивается по адресу 08049211
. Адрес main, 804918d
, находится в этом диапазоне, поэтому вы знаете, что main
является членом текстового раздела. Если вы вычтите основание текстового раздела из адреса main, вы обнаружите, что main составляет 13d
байт в текстовом разделе. Список раздела также содержит смещение файла, с которого начинаются данные для текстового раздела. Это 1050
, поэтому первый байт main находится по смещению 0x1050 + 0x13d == 0x118d
.
Вы можете сделать то же самое вычисление, используя заголовки программы:
$ readelf --program-headers test
[...]
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00160 0x00160 R 0x4
INTERP 0x000194 0x08048194 0x08048194 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x002e8 0x002e8 R 0x1000
LOAD 0x001000 0x08049000 0x08049000 0x00228 0x00228 R E 0x1000
LOAD 0x002000 0x0804a000 0x0804a000 0x0019c 0x0019c R 0x1000
LOAD 0x002f0c 0x0804bf0c 0x0804bf0c 0x00110 0x00114 RW 0x1000
[...]
Вторая строка загрузки сообщает вам, что область от 08049000
(VirtAddr) до 08049228
(VirtAddr + MemSiz) является читаемой и исполняемой и загружается со смещения 1000
в файле. Итак, вы снова можете вычислить, что адрес main составляет 18d
байт в этой области загрузки, поэтому он должен находиться по смещению 0x118d
внутри исполняемого файла. Давайте проверим, что:
$ ./test
Hello world!
$ echo -ne '\xc3' | dd of=test conv=notrunc bs=1 count=1 seek=$((0x118d))
1+0 records in
1+0 records out
1 byte copied, 0.0116672 s, 0.1 kB/s
$ ./test
$
Перезапись первого байта main с помощью 0xc3
, кода операции return (near) на x86, приводит к тому, что программа больше ничего не выводит.