Сомнения в исполняемом и перемещаемом объектном файле

Я записал простому Привет Мировую программу.

   #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. Что то, что я понимаю превратно?

Спасибо. Бала

7
задан skaffman 6 May 2010 в 22:23
поделиться

3 ответа

В первом случае посмотрите на кодировку инструкции - там, где должен быть адрес функции, одни нули. Это потому, что объект еще не был слинкован, поэтому адреса внешних символов еще не были подключены. Когда вы выполняете окончательное связывание в исполняемый формат, система помещает туда еще один заполнитель, а затем динамический компоновщик окончательно добавит правильный адрес для 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. Это перемещение будет замечено во время компоновки и исправлено. В случае динамического связывания библиотек перемещение и исправление могут быть не полностью решены до тех пор, пока программа не будет запущена, но вы, надеюсь, поймете идею из этого примера.

7
ответ дан 6 December 2019 в 09:58
поделиться

Вызовы являются относительными в x86, IIRC, если у вас есть e8, местоположение вызова - addr + 5.

e1 fe ff ff a - относительный переход с прямым порядком байтов. На самом деле это означает fffffee1 .

Теперь добавьте это к адресу инструкции вызова + 5: (0xfffffee1 + 0x4004da + 5)% 2 ** 32 = 0x4003c0

5
ответ дан 6 December 2019 в 09:58
поделиться

Цель вызова в инструкции 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 в двоичном формате. Это именно то, что вы видите в машинном коде. Просто используемый вами инструмент отображает его в обратном порядке.

7
ответ дан 6 December 2019 в 09:58
поделиться
Другие вопросы по тегам:

Похожие вопросы: