Ваш gcc по умолчанию создает исполняемые файлы PIE ( 32-разрядные абсолютные адреса больше не разрешены в x86-64 Linux? ).
Я не уверен, почему, но при этом компоновщик автоматически не разрешает call puts
в call puts@plt
. Все еще генерируется запись puts
PLT, но call
не идет туда.
Во время выполнения динамический компоновщик пытается разрешить puts
непосредственно в символ libc этого имени и исправить call rel32
. Но символ больше + -2 ^ 31, поэтому мы получаем предупреждение о переполнении перемещения R_X86_64_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
путем , перепрыгивая на него вместо вызова, с той же позицией стека, что и при входе в эту функцию. Это работает с или без пирога. Просто замените call
на jmp
, если RSP указывает на ваш собственный обратный адрес.
Если вы хотите сделать исполняемые файлы PIE, у вас есть два варианта
call puts wrt ..plt
- явный звонок через PLT.
call [rel puts wrt ..got]
- явно выполнить косвенный вызов через запись GOT, как в gcc в стиле -fno-plt
code-gen. (Использование режима 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 ; remove the padding
ret
В исполняемом файле в зависимости от позиции вы можете использовать mov edi, message
вместо REA-относительного LEA. Он имеет меньший размер кода и может работать на большем количестве портов исполнения на большинстве процессоров.
В исполняемом файле, отличном от 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