Как разделяемая библиотека находит раздел GOT?
Пока я читал http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/ пришел вопрос:
Как разделяемая библиотека PIC после загрузки где-нибудь в виртуальном адресном пространстве процесса знает, как ссылаться на внешние переменные?
Вот код обсуждаемой библиотеки:
#include <stdio.h>
extern long var;
void
shara_func(void)
{
printf("%ld\n", var);
}
Создайте объектный код, затем общий объект (библиотека):
gcc -fPIC -c lib1.c # produce PIC lib1.o
gcc -fPIC -shared lib1.o -o liblib1.so # produce PIC shared library
разбирать shara_func
в общей библиотеке:
objdump -d liblib1.so
...
00000000000006d0 <shara_func>:
6d0: 55 push %rbp
6d1: 48 89 e5 mov %rsp,%rbp
6d4: 48 8b 05 fd 08 20 00 mov 0x2008fd(%rip),%rax # 200fd8 <_DYNAMIC+0x1c8>
6db: 48 8b 00 mov (%rax),%rax
6de: 48 89 c6 mov %rax,%rsi
6e1: 48 8d 3d 19 00 00 00 lea 0x19(%rip),%rdi # 701 <_fini+0x9>
6e8: b8 00 00 00 00 mov $0x0,%eax
6ed: e8 be fe ff ff callq 5b0 <printf@plt>
6f2: 90 nop
6f3: 5d pop %rbp
6f4: c3 retq
...
Я вижу, что инструкция по адресу 0x6d4 перемещает некоторый адрес относительно ПК в rax
Я полагаю, что это запись в GOT, GOT ссылается относительно ПК, чтобы получить адрес внешней переменной var
во время выполнения (это решается во время выполнения в зависимости от того, где var
был загружен). Затем после выполнения инструкции в 0x6db мы получаем фактическое содержимое внешней переменной, помещенное в rax, затем перемещаем значение из rax в rsi - второй параметр функции, переданный в регистр.
Я думал, что есть только один GOT в памяти процесса, однако, видите, что ссылки на библиотеки GOT? Как разделяемая библиотека знает смещение к GOT процесса, когда она (библиотека PIC) не знает, где в памяти процесса она будет загружена? Или у каждой разделяемой библиотеки есть свой GOT, который загружен вместе с ней? Я был бы очень рад, если вы проясните мою путаницу.
1 ответ
Я думал, что есть только один GOT в памяти процесса, однако, видите, что ссылки на библиотеки GOT?
Мы ясно видим .got
раздел как часть библиотеки. С readelf
мы можем найти, какие разделы библиотеки и как они загружены:
readelf -e liblib1.so
...
Section Headers:
[21] .got PROGBITS 0000000000200fd0 00000fd0
0000000000000030 0000000000000008 WA 0 0 8
...
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x000000000000078c 0x000000000000078c R E 200000
LOAD 0x0000000000000df8 0x0000000000200df8 0x0000000000200df8
0x0000000000000230 0x0000000000000238 RW 200000
...
Section to Segment mapping:
Segment Sections...
00 ... .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
Итак, есть раздел .got
, но компоновщик времени выполненияld-linux.so.2
(зарегистрирован как интерпретатор для динамических ELF) не загружает разделы; загружает сегменты, как описано в заголовке программы с LOAD
тип. .got
является частью сегмента 01 LOAD с флагами RW. Другая библиотека будет иметь собственный GOT (подумайте о компиляции liblib2.so из аналогичного источника, она ничего не будет знать о liblib1.so и будет иметь собственный GOT); так что "Глобальный" только для библиотеки; но не на весь образ программы в памяти после загрузки.
Как разделяемая библиотека знает смещение к GOT процесса, когда она (библиотека PIC) не знает, где в памяти процесса она будет загружена?
Это делается статическим компоновщиком, когда он берет несколько объектов ELF и объединяет их все в одну библиотеку. Линкер сгенерирует .got
раздел и поместите его в какое-то место с известным смещением от кода библиотеки (pc-относительный, rip-относительный). Он записывает инструкции в заголовок программы, поэтому относительный адрес известен, и это единственный необходимый адрес для доступа к собственному GOT.
когда objdump
используется с -r
/ -R
flags, он будет печатать информацию о перемещениях (статических / динамических), записанных в ELF-файле или библиотеке; это может быть объединено с флагом -d. объект lib1.o перенес сюда; нет известного смещения GOT, mov имеет все нули:
$ objdump -dr lib1.o
lib1.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <shara_func>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # b <shara_func+0xb>
7: R_X86_64_REX_GOTPCRELX var-0x4
b: 48 8b 00 mov (%rax),%rax
e: 48 89 c6 mov %rax,%rsi
В файле библиотеки это было преобразовано в относительный адрес gcc -shared
(это вызывает ld
вариант collect2
внутри):
$ objdump -d liblib1.so
liblib1.so: file format elf64-x86-64
00000000000006d0 <shara_func>:
6d0: 55 push %rbp
6d1: 48 89 e5 mov %rsp,%rbp
6d4: 48 8b 05 fd 08 20 00 mov 0x2008fd(%rip),%rax # 200fd8 <_DYNAMIC+0x1c8>
И, наконец, есть динамическое перемещение в GOT, чтобы поместить здесь фактический адрес var (выполняется rtld - ld-linux.so.2):
$ objdump -R liblib1.so
liblib1.so: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
...
0000000000200fd8 R_X86_64_GLOB_DAT var
Давайте используем вашу библиотеку, добавим исполняемый файл с определением, скомпилируем его и запустим с включенной отладкой rtld:
$ cat main.c
long var;
int main(){
shara_func();
return 0;
}
$ gcc main.c -llib1 -L. -o main -Wl,-rpath=`pwd`
$ LD_DEBUG=all ./main 2>&1 |less
...
311: symbol=var; lookup in file=./main [0]
311: binding file /test3/liblib1.so [0] to ./main [0]: normal symbol `var'
Таким образом, компоновщик смог связать перемещение для var
в "основной" файл ELF, где он определен:
$ gdb -q ./main
Reading symbols from ./main...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x4006da
(gdb) r
Starting program: /test3/main
Breakpoint 1, 0x00000000004006da in main ()
(gdb) disassemble shara_func
Dump of assembler code for function shara_func:
0x00007ffff7bd56d0 <+0>: push %rbp
0x00007ffff7bd56d1 <+1>: mov %rsp,%rbp
0x00007ffff7bd56d4 <+4>: mov 0x2008fd(%rip),%rax # 0x7ffff7dd5fd8
0x00007ffff7bd56db <+11>: mov (%rax),%rax
0x00007ffff7bd56de <+14>: mov %rax,%rsi
Никаких изменений в mov в вашем функционале. rax после func+4 равен 0x601040, это третье отображение./main согласно /proc/$pid/maps:
00601000-00602000 rw-p 00001000 08:07 6691394 /test3/main
И он был загружен из main после этого заголовка программы (readelf -e ./main)
LOAD 0x0000000000000df0 0x0000000000600df0 0x0000000000600df0
0x0000000000000248 0x0000000000000258 RW 200000
Это часть раздела.bss:
[26] .bss NOBITS 0000000000601038 00001038
0000000000000010 0000000000000000 WA 0 0 8
После перехода к func+11 мы можем проверить значение в GOT:
(gdb) b shara_func
(gdb) r
(gdb) si
0x00007ffff7bd56db in shara_func () from /test3/liblib1.so
1: x/i $pc
=> 0x7ffff7bd56db <shara_func+11>: mov (%rax),%rax
(gdb) p $rip+0x2008fd
$6 = (void (*)()) 0x7ffff7dd5fd8
(gdb) x/2x 0x7ffff7dd5fd8
0x7ffff7dd5fd8: 0x00601040 0x00000000
Кто написал правильное значение для этой записи?
(gdb) watch *0x7ffff7dd5fd8
Hardware watchpoint 2: *0x7ffff7dd5fd8
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /test3/main
Hardware watchpoint 2: *0x7ffff7dd5fd8
Old value = <unreadable>
New value = 6295616
0x00007ffff7de36bf in elf_machine_rela (..) at ../sysdeps/x86_64/dl-machine.h:435
(gdb) bt
#0 0x00007ffff7de36bf in elf_machine_rela (...) at ../sysdeps/x86_64/dl-machine.h:435
#1 elf_dynamic_do_Rela (...) at do-rel.h:137
#2 _dl_relocate_object (...) at dl-reloc.c:258
#3 0x00007ffff7ddaf5b in dl_main (...) at rtld.c:2072
#4 0x00007ffff7df0462 in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffde20,
dl_main=dl_main@entry=0x7ffff7dd89a0 <dl_main>) at ../elf/dl-sysdep.c:249
#5 0x00007ffff7ddbe7a in _dl_start_final (arg=0x7fffffffde20) at rtld.c:307
#6 _dl_start (arg=0x7fffffffde20) at rtld.c:413
#7 0x00007ffff7dd7cc8 in _start () from /lib64/ld-linux-x86-64.so.2
(gdb) x/2x 0x7ffff7dd5fd8
0x7ffff7dd5fd8: 0x00601040 0x00000000
Линкер выполнения glibc (rtld.c), непосредственно перед вызовом main
- вот источник (немного другая версия) - http://code.metager.de/source/xref/gnu/glibc/sysdeps/x86_64/dl-machine.h
329 case R_X86_64_GLOB_DAT:
330 case R_X86_64_JUMP_SLOT:
331 *reloc_addr = value + reloc->r_addend;
332 break;
При обратном шаге мы можем получить историю кода и старое значение = 0:
(gdb) b _dl_relocate_object
(gdb) r
(gdb) dis 3
(gdb) target record-full
(gdb) c
(gdb) disp/i $pc
(gdb) rsi
(gdb) rsi
(gdb) rsi
(gdb) x/2x 0x7ffff7dd5fd8
0x7ffff7dd5fd8: 0x00000000 0x00000000
=> 0x7ffff7de36b8 <_dl_relocate_object+1560>: add 0x10(%rbx),%rax
=> 0x7ffff7de36bc <_dl_relocate_object+1564>: mov %rax,(%r10)
=> 0x7ffff7de36bf <_dl_relocate_object+1567>: nop