Связать программу с помощью printf с ld?

Для завершения в ES6 у нас есть классы (поддерживаемые на момент написания этого только в последних браузерах, но доступные в Babel, TypeScript и других транспилерах)

class Foo {
  constructor(){
    this.a = 5;
    this.b = 6;
    this.c = this.a + this.b;
  }  
}

const foo = new Foo();
1
задан Peter Cordes 23 March 2019 в 16:57
поделиться

1 ответ

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 может запускать свои функции init через ловушки динамического компоновщика . Это не безопасно в 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-битные исполняемые файлы с расширенными знаками или нулями. Но REA-относительный 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. Это оставлено в качестве упражнения для читателя.)

[114 ]
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
0
ответ дан Peter Cordes 23 March 2019 в 16:57
поделиться
Другие вопросы по тегам:

Похожие вопросы: