Связать программу с помощью printf с ld? - PullRequest
1 голос
/ 23 марта 2019

Я получаю undefined reference to _printf при сборке программы сборки, которая определяет собственный _start вместо main, используя NASM на x86-64 Ubuntu

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

   nasm -f elf64 hello.asm
   ld -s -o hello hello.o
   hello.o: In function `_start':
   hello.asm:(.text+0x1a): undefined reference to `_printf'
   MakeFile:4: recipe for target 'compile' failed
   make: *** [compile] Error 1

источник носа:

extern _printf

section .text
    global _start
_start:
    mov rdi, format     ; argument #1
    mov rsi, message    ; argument #2
    mov rax, 0
  call _printf            ; call printf

    mov rax, 0
    ret                 ; return 0

section .data

    message:    db "Hello, world!", 0
    format:   db "%s", 0xa, 0

Привет, мир! должен быть вывод

1 Ответ

4 голосов
/ 23 марта 2019

3 задачи:

  • GNU / Linux с использованием объектных файлов ELF не украшает / искажает имена C с начальным подчеркиванием. Используйте call printf, а не _printf (В отличие от MacOS X, который украшает символы _; имейте это в виду, если вы смотрите учебные пособия для других ОС. Windows также использует другое соглашение о вызовах, но только 32-разрядные Windows изменяют имена с помощью _ или других декораций, которые кодируют выбор соглашения о вызовах.)

  • Вы не сказали ld, чтобы связать libc , и вы не определили printf самостоятельно, поэтому вы не дали компоновщику входные файлы, содержащие определение для этого символа. printf является библиотечной функцией, определенной в libc.so, и, в отличие от внешнего интерфейса GCC, ld не включает ее автоматически.

  • _start не является функцией, вы не можете ret из нее. RSP указывает на argc, а не обратный адрес. Вместо этого определите main, если хотите, чтобы она была нормальной функцией.

Ссылка с gcc -no-pie -nostartfiles hello.o -o hello, если вы хотите динамический исполняемый файл, который предоставляет собственный _start вместо main, но все еще использует libc.


Это безопасно для динамических исполняемых файлов в GNU / Linux, потому что glibc может запускать свои функции инициализации через ловушки динамического компоновщика . Это небезопасно на Cygwin, где его libc инициализируется только вызовами из его стартового файла CRT (которые делают это до вызова main).

Используйте call exit для выхода, вместо того, чтобы делать системный вызов _exit напрямую, если вы используете printf; это позволяет libc сбрасывать любые буферизованные выходные данные. (Если вы перенаправите вывод в файл, stdout будет иметь полную буферизацию, а не строку с буферизацией на терминале.)

-static не будет безопасным; в статическом исполняемом файле динамический компоновщик не запускается до вашего _start, поэтому у libc нет возможности инициализировать себя, если вы не вызовете функции вручную. Это возможно, но обычно не рекомендуется.

Существуют другие реализации libc, которым не нужны никакие функции инициализации, вызываемые до того, как printf / malloc / другие функции сработают. В glibc такие вещи, как буферы stdio, выделяются во время выполнения. (Это раньше имело место для MUSL libc , но это, очевидно, уже не так, согласно комментарию Флориана к этому ответу.)


Обычно, если вы хотите использовать функции libc, рекомендуется определить функцию main вместо вашей собственной точки входа _start. Тогда вы можете просто связать с gcc обычно, без специальных опций.

См. Какие части этого ассемблерного кода HelloWorld необходимы, если бы мне пришлось писать программу в ассемблере? для этого и версии, которая использует системные вызовы Linux напрямую, без libc.


Если вы хотите, чтобы ваш код работал в исполняемом файле PIE, как это делает gcc по умолчанию (без --no-pie) на последних дистрибутивах, вам понадобится call printf wrt ..plt.

В любом случае, вы должны использовать lea rsi, [rel message], потому что REA-относительный LEA более эффективен, чем mov r64, imm64 с 64-битным абсолютным адресом. (В позиционно-зависимом коде лучшим вариантом для помещения статического адреса в 64-битный регистр является 5-байтовый mov esi, message, поскольку известно, что статические адреса в исполняемых файлах, отличных от PIE, находятся на низком 2 ГБ виртуального адресного пространства, и, таким образом, работают как 32-битные исполняемые файлы со знаком или с нулевым расширением. Но RIP-родственник LEA не намного хуже и работает везде.)

;;; Defining your own _start but using libc
;;; works on Linux for non-PIE executables

default rel                ; Use RIP-relative for [symbol] addressing modes
extern printf
extern exit                ; unlike _exit, exit flushes stdio buffers

section .text
    global _start
_start:
    ;; RSP is already aligned by 16 on entry at _start, unlike in functions

    lea    rdi, [format]        ; argument #1   or better  mov edi, format
    lea    rsi, [message]       ; argument #2
    xor    eax, eax             ; no FP args to the variadic function
    call   printf               ; for a PIE executable:  call printf wrt ..plt

    xor    edi, edi             ; arg #1 = 0
    call   exit                 ; exit(0)
    ; exit definitely does not return

section .rodata        ;; read-only data can go in .rodata instead of read-write .data

    message:    db "Hello, world!", 0
    format:   db "%s", 0xa, 0

Соберите нормально, ссылка с gcc -no-pie -nostartfiles hello.o. Это исключает файлы запуска CRT, которые обычно определяют _start, который делает некоторые вещи перед вызовом main. Функции инициализации Libc вызываются из ловушек динамического компоновщика, поэтому printf можно использовать.

Это было бы не так с gcc -static -nostartfiles hello.o. Я включил примеры того, что происходит, если вы используете неправильные параметры:

peter@volta:/tmp$ nasm -felf64 nopie-start.asm 
peter@volta:/tmp$ gcc -no-pie -nostartfiles nopie-start.o 
peter@volta:/tmp$ ./a.out 
Hello, world!
peter@volta:/tmp$ file a.out 
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0cd1cd111ba0c6926d5d69f9191bdf136e098e62, not stripped

# link error without -no-pie because it doesn't automatically make PLT stubs
peter@volta:/tmp$ gcc -nostartfiles nopie-start.o 
/usr/bin/ld: nopie-start.o: relocation R_X86_64_PC32 against symbol `printf@@GLIBC_2.2.5' can not be used when making a PIE object; recompile with -fPIC
/usr/bin/ld: final link failed: bad value
collect2: error: ld returned 1 exit status


# runtime error with -static
peter@volta:/tmp$ gcc -static -no-pie -nostartfiles nopie-start.o -o static_start-hello
peter@volta:/tmp$ ./static_start-hello 
Segmentation fault (core dumped)

Альтернативная версия, определяющая main вместо _start

(и упрощение с использованием puts вместо printf.)

default rel                ; Use RIP-relative for [symbol] addressing modes
extern puts

section .text
    global main
main:
    sub    rsp, 8    ;; RSP was 16-byte aligned *before* a call pushed a return address
                     ;; RSP is now 16-byte aligned, ready for another call

    mov    edi, message         ; argument #1, optimized to use non-PIE-only move imm32
    call   puts

    add    rsp, 8               ; restore the stack
    xor    eax, eax             ; return 0
    ret

section .rodata
    message:    db "Hello, world!", 0     ; puts appends a newline

puts в значительной степени точно реализует printf("%s\n", string);Компиляторы C сделают эту оптимизацию за вас, но в asm вы должны сделать это самостоятельно.

Ссылка с gcc -no-pie hello.o, или даже статическая ссылка с использованием gcc -no-pie -static hello.o.Код запуска CRT будет вызывать функции инициализации glibc.

peter@volta:/tmp$ nasm -felf64 nopie-main.asm 
peter@volta:/tmp$ gcc -no-pie nopie-main.o 
peter@volta:/tmp$ ./a.out 
Hello, world!

# link error if you leave out -no-pie  because of the imm32 absolute address
peter@volta:/tmp$ gcc nopie-main.o 
/usr/bin/ld: nopie-main.o: relocation R_X86_64_32 against `.rodata' can not be used when making a PIE object; recompile with -fPIC
/usr/bin/ld: final link failed: nonrepresentable section on output
collect2: error: ld returned 1 exit status

main - это функция, поэтому перед повторным вызовом функции необходимо заново выровнять стек.Фиктивный толчок также является допустимым способом выравнивания стека при входе в функцию, но add / sub rsp, 8 более понятен.

Альтернативой является jmp puts для его хвостового вызова, поэтому возврат mainзначение будет равно puts, возвращаемому.В этом случае вы должны , а не изменить rsp в первую очередь: вы просто переходите к puts с вашим обратным адресом в стеке, точно так же, как если бы ваш вызывающий абонент вызвал puts.


PIE-совместимый код, определяющий main

(Вы можете сделать PIE, который определяет свой собственный _start. Это оставлено в качестве упражнения для читателя.)

default rel                ; Use RIP-relative for [symbol] addressing modes
extern puts

section .text
    global main
main:
    sub    rsp, 8    ;; RSP was 16-byte aligned *before* a call pushed a return address

    lea    rdi, [message]         ; argument #1
    call   puts  wrt ..plt

    add    rsp, 8
    xor    eax, eax               ; return 0
    ret

section .rodata
    message:    db "Hello, world!", 0     ; puts appends a newline
peter@volta:/tmp$ nasm -felf64 pie.asm
peter@volta:/tmp$ gcc pie.o
peter@volta:/tmp$ ./a.out 
Hello, world!
peter@volta:/tmp$ file a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b27e6032f955d628a542f6391b50805c68541fb9, not stripped
...