Невозможно вызвать функцию стандартной библиотеки C в 64-битном Linux из кода сборки (yasm)

У меня есть функция foo, написанная на ассемблере и скомпилированная с помощью yasm и GCC для 64-разрядной версии Linux (Ubuntu). Он просто выводит сообщение на стандартный вывод, используя 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 генерирует аналогичный код?


person szx    schedule 01.09.2018    source источник


Ответы (2)


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

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

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

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

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

person lockcmpxchg8b    schedule 01.09.2018

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.soPC32 релокации. Младшие 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.soGOTPCRELX (расслабляющий)
NASM call [rel bar wrt ..got] использует R_X86.soGOTPCREL (не расслабляющий)

Это меньшая проблема с рукописным 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.soGOTPCRELX.
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
comment
Связано: примеры Hello World из _start и main: Связывание программы с использованием printf с ld? - person Peter Cordes; 28.04.2021