Рассмотрим простую программу 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 к функции, мы все еще делаем два шага вызова:
call [email protected]
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?
Я тоже это видел при написании погрузчиков Elf и т. Д., И, насколько я могу судить, он оптимизирует таблицу перемещений: все внешние функции определяются только один раз, и поэтому нужно переместить только один адрес памяти. В противном случае все вызовы по всему коду, ссылающиеся на одну и ту же внешнюю функцию, должны быть разрешены. Но действительно ли это настоящая причина, я не знаю .. – Kenney
Недостатком Windows ** thunking ** является то, что библиотека не может вызывать функции, экспортируемые исполняемым файлом, не пройдя еще несколько обручей, тогда как на Функции Linux, экспортируемые как библиотеками, так и исполняемыми, входят в общий пул символов. –
Ваше редактирование относится к адресу '0x8049580', который * не * появляется где-либо в вашей первоначальной разборке. Это затрудняет понимание того, к чему обращается ваш «ЗАПИСЬ». Можете ли вы изменить свой вопрос, чтобы адреса и разборка были согласованы *? –