2015-09-20 2 views
4

Рассмотрим простую программу C:Почему импортированные функции называются так косвенно в Linux?

#include <stdio.h> 

int main() 
{ 
    puts("Hello"); 
    return 0; 
} 

Запуск его с помощью GDB, установив LD_BIND_NOW=1 для простоты, я могу наблюдать следующее:

$ gdb -q ./test -ex 'b main' -ex r 
Reading symbols from ./test...done. 
Breakpoint 1 at 0x8048420 
Starting program: /tmp/test 

Breakpoint 1, 0x08048420 in main() 
(gdb) disas 
Dump of assembler code for function main: 
    0x0804841d <+0>:  push ebp 
    0x0804841e <+1>:  mov ebp,esp 
=> 0x08048420 <+3>:  and esp,0xfffffff0 
    0x08048423 <+6>:  sub esp,0x10 
    0x08048426 <+9>:  mov DWORD PTR [esp],0x8048500 
    0x0804842d <+16>: call 0x80482c0 <[email protected]> 
    0x08048432 <+21>: mov eax,0x0 
    0x08048437 <+26>: leave 
    0x08048438 <+27>: ret  
End of assembler dump. 
(gdb) si 4 
0x080482c0 in [email protected]() 
(gdb) disas 
Dump of assembler code for function [email protected]: 
=> 0x080482c0 <+0>:  jmp DWORD PTR ds:0x8049670 
    0x080482c6 <+6>:  push 0x0 
    0x080482cb <+11>: jmp 0x80482b0 
End of assembler dump. 
(gdb) si 
_IO_puts (str=0x8048500 "Hello") at ioputs.c:35 
35  { 
(gdb) 

Видимо, после связывания запись PLT к функции, мы все еще делаем два шага вызова:

  1. call [email protected]
  2. jmp [ds:puts_address]

Сравнивая это с тем, как это реализовано в Win32, все вызовы импортированных функций, например. MessageBoxA, сделаны как

call [ds:MessageBoxA_address] 

т.е. в одну стадию.

Даже если принять во внимание ленивое переплетение, все же возможно иметь, например, [puts_address] содержат вызов _dl_runtime_resolve или что-то еще, что необходимо при запуске, поэтому односторонний косвенный вызов все равно будет работать.

Итак, в чем причина такого осложнения? Является ли это какой-то оптимизацией прогноза ветвления или оптимизацией прогноза ветвления?

EDIT в ответ на Employed Russian's answer (v2)

То, что я на самом деле имею в виду, что это окольные из call PLT; jump [GOT] является излишним, даже в контексте ленивым связывания. Рассмотрим следующий пример (опирается на компиляцию без оптимизации по НКУ):

#include <stdio.h> 

int main() 
{ 
    for(int i=0;i<3;++i) 
    { 
     puts("Hello"); 
     __asm__ __volatile__("nop"); 
    } 
    return 0; 
} 

Запуск его (с LD_BIND_NOW отключенное) в GDB:

$ gdb ./test -ex 'b main' -ex r -ex disas/r 
Reading symbols from ./test...done. 
Breakpoint 1 at 0x8048387 
Starting program: /tmp/test 

Breakpoint 1, 0x08048387 in main() 
Dump of assembler code for function main: 
    ... 
    0x08048397 <+19>: c7 04 24 80 84 04 08 mov DWORD PTR [esp],0x8048480 
    0x0804839e <+26>: e8 11 ff ff ff call 0x80482b4 <[email protected]> 
    0x080483a3 <+31>: 90  nop 
    0x080483a4 <+32>: 83 44 24 1c 01 add DWORD PTR [esp+0x1c],0x1 
    ... 

Дизассемблирование [email protected], мы можем увидеть адрес входа GOT для puts:

(gdb) disas '[email protected]' 
Dump of assembler code for function [email protected]: 
    0x080482b4 <+0>:  jmp DWORD PTR ds:0x8049580 
    0x080482ba <+6>:  push 0x10 
    0x080482bf <+11>: jmp 0x8048284 
End of assembler dump. 

Итак, мы видим, что это 0x8049580. Мы можем залатать наш код для main() изменить e8 11 ff ff ff 90 (адрес 0x8048e9e) для косвенного вызова записи GOT, т.е. call [ds:0x8049580]: ff 15 80 95 04 08:

(gdb) set *(uint64_t*)0x804839e=0x44830804958015ff 
(gdb) disas/r 
Dump of assembler code for function main: 
    ... 
    0x08048397 <+19>: c7 04 24 80 84 04 08 mov DWORD PTR [esp],0x8048480 
    0x0804839e <+26>: ff 15 80 95 04 08  call DWORD PTR ds:0x8049580 
    0x080483a4 <+32>: 83 44 24 1c 01 add DWORD PTR [esp+0x1c],0x1 
    ... 

Запуск программы после того, как это все еще дает:

(gdb) c 
Continuing. 
Hello 
Hello 
Hello 
[Inferior 1 (process 14678) exited normally] 

Т.е. первый вызов сделал ленивую привязку, а следующие два просто использовали результат исправления (вы можете проследить его самостоятельно, если не верите).

Так вопрос остается: почему этот способ вызова не используется GCC?

+0

Я тоже это видел при написании погрузчиков Elf и т. Д., И, насколько я могу судить, он оптимизирует таблицу перемещений: все внешние функции определяются только один раз, и поэтому нужно переместить только один адрес памяти. В противном случае все вызовы по всему коду, ссылающиеся на одну и ту же внешнюю функцию, должны быть разрешены. Но действительно ли это настоящая причина, я не знаю .. – Kenney

+0

Недостатком Windows ** thunking ** является то, что библиотека не может вызывать функции, экспортируемые исполняемым файлом, не пройдя еще несколько обручей, тогда как на Функции Linux, экспортируемые как библиотеками, так и исполняемыми, входят в общий пул символов. –

+0

Ваше редактирование относится к адресу '0x8049580', который * не * появляется где-либо в вашей первоначальной разборке. Это затрудняет понимание того, к чему обращается ваш «ЗАПИСЬ». Можете ли вы изменить свой вопрос, чтобы адреса и разборка были согласованы *? –

ответ

5

По-видимому, после связывания вступления PLT в функцию, мы все еще делаем два шага вызова:

call [email protected] 
jmp [ds:puts_address] 

Компилятор и компоновщик не может знать, что вы собираетесь установить LD_BIND_NOW=1 во время выполнения, и поэтому не может вернуться во времени и переписать сгенерированный код, чтобы использовать прямой call [puts_address].

См. Также recent-fno-pltpatches в списке рассылки gcc-patches.

Win32

Win32 не позволяет разрешение ленивых функций (по крайней мере, не по умолчанию). Другими словами, они компилируют/связывают код, который только работает так, как если бы LD_BIND_NOW=1 был жестко закодирован во время компиляции/ссылки. Некоторая история here.

все еще возможно иметь, например, [puts_address] содержат вызов _dl_runtime_resolve или что-то еще, что необходимо при запуске, поэтому односторонний косвенный вызов все равно будет работать.

Я думаю, вы в замешательстве. [puts_address] содержит _dl_runtime_resolve при запуске (ну, не совсем. Gory details). Ваш вопрос: «Почему звонок не может перейти прямо к [puts_address], почему [email protected] нужен?».

Ответ: _dl_runtime_resolve должен знать который функция разрешительная. Он не может вывести эту информацию из аргументов в puts. Весь raison d'être от [email protected] точно доставляет эту информацию до _dl_runtime_resolve.

Update:

Почему нельзя call <[email protected]> заменить call *[[email protected]].

Ответ содержится в первом -fno-pltpatch я ссылка:

«Это приходит с оговорками Это не может быть вообще сделано для всех функции отмечены EXTERN, как это невозможно для компилятора, если сказать. функция «поистине extern» (определенная в общей библиотеке). Если функция не является действительно внешним (в конечном итоге определяется в конечном исполняемом файле), то , тогда ее косвенное косвенное отношение - это штраф за производительность, поскольку он мог иметь вызов."

Вы могли бы спросить: почему не может линкер (который знает ли puts определяется в том же двоичном или в отдельном DSO) перепишем call *[[email protected]] обратно в call <[email protected]>?

Ответ заключается в том, что это разные инструкции (разные операционные коды), а линкеры обычно не изменяют инструкции, только адреса в инструкциях (в ответ на записи перемещения).

Теоретически линкер мог сделать это, но никто еще не беспокоился.

+0

Ага, последний абзац кажется именно тем, чего мне не хватало. Благодарю. – Ruslan

+0

Хм, подожди. Почему компилятор просто не может вызвать 'call [ds: 0x8049670]' вместо вызова инструкции, которая выполняет 'jmp [ds: 0x8049670]'? – Ruslan

+0

@ Руслан Мы собираемся в кругах. Ответ - «ленивая привязка». Вы действительно должны прочитать сообщение «Gory details», чтобы понять это. –

Смежные вопросы