Я записал простому Привет Мировую программу.
#include <stdio.h>
int main() {
printf("Hello World");
return 0;
}
Я хотел понять, как перемещаемый объектный файл и исполняемый файл похожи. Объектный файл, соответствующий основной функции,
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: b8 00 00 00 00 mov $0x0,%eax
e: e8 00 00 00 00 callq 13 <main+0x13>
13: b8 00 00 00 00 mov $0x0,%eax
18: c9 leaveq
19: c3 retq
Здесь вызов функции для printf является callq 13. Одна вещь, которую я не понимаю, состоит в том, почему это 13. Это означает, вызывают функцию по адресу 13, правильно??. 13 имеет следующую инструкцию, правильно?? Объясните меня, что это означает??
Исполняемый код, соответствующий основному,
00000000004004cc <main>:
4004cc: 55 push %rbp
4004cd: 48 89 e5 mov %rsp,%rbp
4004d0: bf dc 05 40 00 mov $0x4005dc,%edi
4004d5: b8 00 00 00 00 mov $0x0,%eax
4004da: e8 e1 fe ff ff callq 4003c0 <printf@plt>
4004df: b8 00 00 00 00 mov $0x0,%eax
4004e4: c9 leaveq
4004e5: c3 retq
Здесь это - callq 4003c0. Но двоичная инструкция является e8 e1 fe и следующие и следующие. Нет ничего, что соответствует 4003c0. Что то, что я понимаю превратно?
Спасибо. Бала
В первом случае посмотрите на кодировку инструкции - там, где должен быть адрес функции, одни нули. Это потому, что объект еще не был слинкован, поэтому адреса внешних символов еще не были подключены. Когда вы выполняете окончательное связывание в исполняемый формат, система помещает туда еще один заполнитель, а затем динамический компоновщик окончательно добавит правильный адрес для printf()
во время выполнения. Вот небольшой пример для программы "Hello, world", которую я написал.
Сначала разборка объектного файла:
00000000 <_main>:
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 51 push %ecx
e: 83 ec 04 sub $0x4,%esp
11: e8 00 00 00 00 call 16 <_main+0x16>
16: c7 04 24 00 00 00 00 movl $0x0,(%esp)
1d: e8 00 00 00 00 call 22 <_main+0x22>
22: b8 00 00 00 00 mov $0x0,%eax
27: 83 c4 04 add $0x4,%esp
2a: 59 pop %ecx
2b: 5d pop %ebp
2c: 8d 61 fc lea -0x4(%ecx),%esp
2f: c3 ret
Затем перемещения:
main.o: file format pe-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000012 DISP32 ___main
00000019 dir32 .rdata
0000001e DISP32 _puts
Как видите, там есть перемещение для _puts
, в которое превратился вызов printf
. Это перемещение будет замечено во время компоновки и исправлено. В случае динамического связывания библиотек перемещение и исправление могут быть не полностью решены до тех пор, пока программа не будет запущена, но вы, надеюсь, поймете идею из этого примера.
Вызовы являются относительными в x86, IIRC, если у вас есть e8, местоположение вызова - addr + 5.
e1 fe ff ff
a - относительный переход с прямым порядком байтов. На самом деле это означает fffffee1
.
Теперь добавьте это к адресу инструкции вызова + 5:
(0xfffffee1 + 0x4004da + 5)% 2 ** 32 = 0x4003c0
Цель вызова в инструкции E8
(call
) указывается как относительное смещение от текущего значения указателя инструкции (IP).
В вашем первом примере кода смещение очевидно 0x000000
. По сути, это говорит
call +0
Фактический адрес printf
еще не известен, поэтому компилятор просто поместил туда 32-битное значение 0x000000
в качестве заполнителя.
Такой неполный вызов с нулевым смещением естественно будет интерпретирован как вызов текущего значения IP. На вашей платформе IP предварительно инкрементируется, т.е. когда выполняется какая-то инструкция, IP содержит адрес следующей инструкции. Т.е. при выполнении инструкции по адресу 0xE
IP содержит значение 0x13
. А call +0
естественно интерпретируется как вызов инструкции 0x13
. Вот почему вы видите 0x13
при разборке неполного кода.
Когда код завершен, смещение 0x000000
заменяется на фактическое смещение функции printf
в коде. Смещение может быть положительным (вперед) или отрицательным (назад). В вашем случае IP в момент вызова равен 0x4004DF
, а адрес функции printf
- 0x4003C0
. По этой причине машинная инструкция будет содержать 32-битное значение смещения, равное 0x4003C0 - 0x4004DF
, что является отрицательным значением -287
. Поэтому то, что вы видите в коде, на самом деле
call -287
-287
- это 0xFFFFFEE1
в двоичном формате. Это именно то, что вы видите в машинном коде. Просто используемый вами инструмент отображает его в обратном порядке.