TL: DR: 3 варианта:
- Создайте исполняемый файл без PIE (
gcc -no-pie -fno-pie call-lib.c libcall.o
), чтобы компоновщик генерировал для вас запись PLT прозрачно, когда вы пишете call puts
.
call puts wrt ..plt
, как gcc -fPIE
.
call [rel puts wrt ..got]
как gcc -fno-plt
.
Последние два будут работать в исполняемых файлах PIE или общих библиотеках. Третий способ, wrt ..got
, немного более эффективен.
Ваш gcc по умолчанию создает исполняемые файлы PIE (32 -битные абсолютные адреса больше не разрешены в x86-64 Linux?).
Я не уверен, почему, но при этом компоновщик не разрешает автоматически call puts
в call puts@plt
. По-прежнему создается запись puts
PLT, но call
туда не попадает.
Во время выполнения динамический компоновщик пытается разрешить puts
непосредственно в символ libc с этим именем и исправить call rel32
. Но символ удален более чем на + -2 ^ 31, поэтому мы получаем предупреждение о переполнении R_X86.so
PC32
релокации. Младшие 32 бита целевого адреса верны, а старшие - нет. (Таким образом, ваш call
переходит на неправильный адрес).
Ваш код работает для меня, если я использую gcc -no-pie -fno-pie call-lib.c libcall.o
. -no-pie
- это критическая часть: это параметр компоновщика. Команду YASM менять не нужно.
При создании традиционного исполняемого файла, зависящего от позиции, компоновщик превращает символ puts
для цели вызова в puts@plt
для вас, потому что мы связываем динамический исполняемый файл (вместо статического связывания libc с gcc -static -fno-pie
, и в этом случае call
может пойти непосредственно в функцию libc.)
В любом случае, именно поэтому gcc выдает call puts@plt
(синтаксис GAS) при компиляции с -fpie
(по умолчанию на вашем рабочем столе, но не по умолчанию на https://godbolt.org/), но только call puts
при компиляции с -fno-pie
.
См. Что здесь означает @plt? для получения дополнительной информации о PLT, а также К сожалению, состояние динамических библиотек в Linux несколько лет назад. (Современный gcc -fno-plt
похож на одну из идей в том сообщении в блоге.)
Кстати, более точный / конкретный прототип позволил бы gcc избежать обнуления EAX перед вызовом foo
:
extern void foo();
в C означает extern void foo(...);
Вы можете объявить его как extern void foo(void);
, что ()
означает в C ++. C ++ не допускает объявления функций, в которых аргументы остаются неуказанными.
улучшения asm
Вы также можете поместить message
в section .rodata
(данные только для чтения, связанные как часть текстового сегмента).
Вам не нужен фрейм стека, просто что-то, чтобы выровнять стек на 16 перед вызовом. Манекен push rax
сделает это.
Или мы можем выполнить хвостовой вызов puts
, перескочив на него вместо его вызова, с той же позицией в стеке, что и при входе в эту функцию. Это работает с PIE или без него. Просто замените call
на jmp
, если RSP указывает на ваш собственный обратный адрес.
Если вы хотите создавать исполняемые файлы PIE (или разделяемые библиотеки), у вас есть два варианта
call puts wrt ..plt
- явно звонить через PLT.
call [rel puts wrt ..got]
- явно выполнить косвенный вызов через запись GOT, как в стиле gcc -fno-plt
генерации кода. (Использование режима адресации относительно RIP для доступа к GOT, отсюда и ключевое слово rel
).
WRT = С уважением. Руководство по NASM документы wrt ..plt
, а также см. раздел 7.9.3: специальные символы и WRT.
Обычно вы должны использовать default rel
в верхней части файла, чтобы вы могли фактически использовать call [puts wrt ..got]
и при этом получить режим адресации, относящейся к RIP. Вы не можете использовать 32-битный режим абсолютной адресации в коде PIE или PIC.
call [puts wrt ..got]
ассемблируется в косвенный вызов памяти, используя указатель функции, который динамически связывается с GOT. (Раннее связывание, а не ленивое динамическое связывание.)
NASM документы ..got
для получения адреса переменных в разделе 9.2.3 . Функции в (других) библиотеках идентичны: вы получаете указатель от GOT вместо прямого вызова, потому что смещение не является константой времени компоновки и может не соответствовать 32-битным.
YASM также принимает call [puts wrt ..GOTPCREL]
, как и синтаксис AT&T call *puts@GOTPCREL(%rip)
, но NASM - нет.
; don't use BITS 64. You *want* an error if you try to assemble this into a 32-bit .o
default rel ; RIP-relative addressing instead of 32-bit absolute by default; makes the [rel ...] optional
section .rodata ; .rodata is best for constants, not .data
message:
db 'foo() called', 0
section .text
global foo
foo:
sub rsp, 8 ; align the stack by 16
; PIE with PLT
lea rdi, [rel message] ; needed for PIE
call puts WRT ..plt ; tailcall puts
;or
; PIE with -fno-plt style code, skips the PLT indirection
lea rdi, [rel message]
call [rel puts wrt ..got]
;or
; non-PIE
mov edi, message ; more efficient, but only works in non-PIE / non-PIC
call puts ; linker will rewrite it into call puts@plt
add rsp,8 ; restore the stack, undoing the add
ret
В исполняемом файле Linux, зависимом от позиции, вы можете использовать mov edi, message
вместо относительного RIP LEA. Он имеет меньший размер кода и может работать на большем количестве портов выполнения на большинстве процессоров. (Интересный факт: MacOS всегда помещает базу изображений за пределы нижних 4 ГиБ, поэтому такая оптимизация там невозможна.)
В исполняемом файле, отличном от PIE, вы также можете использовать call puts
или jmp puts
и позволить компоновщику разобраться с этим, если вам не нужна более эффективная динамическая компоновка в стиле no-plt. Но если вы решите статически связать libc, я думаю, это единственный способ получить прямой jmp для функции libc.
(Я думаю, что возможность статического связывания для не-PIE, почему ld
готова автоматически генерировать заглушки PLT для не-PIE, но не для PIE или общих библиотек. Это требует, чтобы вы сказали, что вы имеете в виду при связывании общих объектов ELF.)
Если вы действительно использовали call puts
в PIE (call rel32
), это могло бы работать только в том случае, если вы статически связали независимую от позиции реализацию puts
в PIE, так что все это было одним исполняемым файлом, который будет загружаться по случайному адресу во время выполнения ( обычный механизм динамического компоновщика), но просто не зависел от libc.so.6
Компоновщик ослабляет вызовы, когда цель присутствует во время статической ссылки
GAS call *bar@GOTPCREL(%rip)
использует R_X86.so
GOTPCRELX
(расслабляющий)
NASM call [rel bar wrt ..got]
использует R_X86.so
GOTPCREL
(не расслабляющий)
Это меньшая проблема с рукописным asm; вы можете просто использовать call bar
, если знаете, что символ будет присутствовать в другом .o
(а не .so
), на который вы собираетесь связать. Но компиляторы C не знают разницы между библиотечными функциями и другими пользовательскими функциями, которые вы объявляете с помощью прототипов (если вы не используете такие вещи, как gcc -fvisibility=hidden
https://gcc.gnu.org/wiki/Visibility или attributes / pragmas).
Тем не менее, вы можете захотеть написать asm-источник, который компоновщик может оптимизировать, если вы статически связываете библиотеку, но, AFAIK, вы не можете сделать это с NASM. Вы можете экспортировать символ как скрытый (видимый во время статической ссылки, но не для динамического связывания в окончательном .so) с помощью global bar:function hidden
, но это в исходном файле, определяющем функцию, а не в файлах, обращающихся к нему.
global bar
bar:
mov eax,231
syscall
call bar wrt ..plt
call [rel bar wrt ..got]
extern bar
2-й файл, после сборки с nasm -felf64
и разборки с objdump -drwc -Mintel
, чтобы увидеть релокации:
0000000000000000 <.text>:
0: e8 00 00 00 00 call 0x5 1: R_X86_64_PLT32 bar-0x4
5: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # 0xb 7: R_X86_64_GOTPCREL bar-0x4
После компоновки с ld
(GNU Binutils) 2.35.1 - ld bar.o bar2.o -o bar
0000000000401000 <_start>:
401000: e8 0b 00 00 00 call 401010 <bar>
401005: ff 15 ed 1f 00 00 call QWORD PTR [rip+0x1fed] # 402ff8 <.got>
40100b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
0000000000401010 <bar>:
401010: b8 e7 00 00 00 mov eax,0xe7
401015: 0f 05 syscall
Обратите внимание, что форма PLT была упрощена до прямого call bar
, PLT исключен. Но ff 15
вызов [rel mem] не преобразован в e8 rel32
С ГАЗОМ:
_start:
call bar@plt
call *bar@GOTPCREL(%rip)
gcc -c foo.s && disas foo.o
0000000000000000 <_start>:
0: e8 00 00 00 00 call 5 <_start+0x5> 1: R_X86_64_PLT32 bar-0x4
5: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # b <_start+0xb> 7: R_X86_64_GOTPCRELX bar-0x4
Обратите внимание на X в конце R_X86.so
GOTPCRELX.
ld bar2.o foo.o -o bar && disas bar
:
0000000000401000 <bar>:
401000: b8 e7 00 00 00 mov eax,0xe7
401005: 0f 05 syscall
0000000000401007 <_start>:
401007: e8 f4 ff ff ff call 401000 <bar>
40100c: 67 e8 ee ff ff ff addr32 call 401000 <bar>
Оба вызова были переведены в прямой e8
call rel32
адрес назначения. Дополнительный байт при косвенном вызове заполняется префиксом размера адреса 67
(который не влияет на call rel32
), дополняя инструкцию до той же длины. (Потому что уже слишком поздно собирать и повторно вычислять все относительные ветви внутри функций, выравнивания и т. Д.)
Это могло бы произойти для call *puts@GOTPCREL(%rip)
, если бы вы статически связали libc с gcc -static
.
person
Peter Cordes
schedule
01.09.2018