Невозможно вызвать стандартную библиотеку C на 64-битном Linux из кода сборки (yasm) - PullRequest
0 голосов
/ 01 сентября 2018

У меня есть функция foo, написанная на ассемблере и скомпилированная с помощью yasm и GCC в Linux (Ubuntu), 64-битная. Он просто печатает сообщение на стандартный вывод, используя puts(), вот как это выглядит:

bits 64

extern puts
global foo

section .data

message:
  db 'foo() called', 0

section .text

foo:
  push rbp
  mov rbp, rsp
  lea rdi, [rel message]
  call puts
  pop rbp
  ret

Вызывается программой на C, скомпилированной с GCC:

extern void foo();

int main() {
    foo();
    return 0;
}

Команды сборки:

yasm -f elf64 foo_64_unix.asm
gcc -c foo_main.c -o foo_main.o
gcc foo_64_unix.o foo_main.o -o foo
./foo

Вот проблема:

При запуске программы она выводит сообщение об ошибке и сразу же вызывает ошибки во время вызова на puts:

./foo: Symbol `puts' causes overflow in R_X86_64_PC32 relocation
Segmentation fault

После разборки с помощью objdump я вижу, что звонок сделан с неправильным адресом:

0000000000000660 <foo>:
 660:   90                      nop
 661:   55                      push   %rbp
 662:   48 89 e5                mov    %rsp,%rbp
 665:   48 8d 3d a4 09 20 00    lea    0x2009a4(%rip),%rdi
 66c:   e8 00 00 00 00          callq  671 <foo+0x11>      <-- here
 671:   5d                      pop    %rbp
 672:   c3                      retq

(671 - это адрес следующей инструкции, а не адрес puts)

Однако, если я переписываю тот же код в C, вызов выполняется по-другому:

645:   e8 c6 fe ff ff          callq  510 <puts@plt>

т.е. он ссылается на puts из PLT.

Можно ли сказать yasm сгенерировать подобный код?

Ответы [ 2 ]

0 голосов
/ 01 сентября 2018

Ваш 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

0 голосов
/ 01 сентября 2018

очищенная версия удаленных комментариев

IIRC, за кодом операции 0xe8 следует смещение со знаком, которое будет применено к ПК (который к тому времени перешел к следующей инструкции) для вычисления цели перехода. Следовательно, objdump интерпретирует цель перехода как 0x671.

Yasm рендерит нули, потому что он, вероятно, поместил перемещение в это смещение, и именно поэтому он запрашивает у загрузчика правильное смещение для puts во время загрузки. Загрузчик сталкивается с переполнением при вычислении перемещения, что может указывать на то, что puts на большее смещение от вашего вызова, чем может быть представлено в 32-битном смещении со знаком. Следовательно, загрузчик не может исправить эту инструкцию, и вы получаете сбой.

66c: e8 00 00 00 00 показывает ненаселенный адрес. Если вы заглянете в свою таблицу перемещений, вы должны увидеть перемещение на 0x66d. Нередко ассемблер заполняет адреса / смещения с помощью перемещений как все нули.

Эта страница предполагает, что YASM имеет директиву WRT, которая может контролировать использование .got, .plt и т. Д.

Согласно S9.2.5 на этой странице похоже, что вы можете сказать CALL puts WRT ..plt (при условии, что Yasm использует тот же синтаксис, что и справочник NASM)

...