Сборка 32-битных двоичных файлов в 64-битной системе (инструментальная цепочка GNU)

Я пишу код сборки, который можно скомпилировать:

as power.s -o power.o

возникает проблема, когда я связываю файл объекта power.o:

ld power.o -o power

Чтобы работать на 64-битной ОС (Ubuntu 14.04), я добавил .code32 в начале файла power.s, но все равно получаю ошибку:

Ошибка сегментации (сброс ядра)

power.s:

.code32
.section .data
.section .text
.global _start
_start:
pushl $3
pushl $2 
call power 
addl $8, %esp
pushl %eax 

pushl $2
pushl $5
call power
addl $8, %esp

popl %ebx
addl %eax, %ebx

movl $1, %eax
int $0x80



.type power, @function
power:
pushl %ebp  
movl %esp, %ebp 
subl $4, %esp 
movl 8(%ebp), %ebx 
movl 12(%ebp), %ecx 
movl %ebx, -4(%ebp) 

power_loop_start:
cmpl $1, %ecx 
je end_power
movl -4(%ebp), %eax
imull %ebx, %eax
movl %eax, -4(%ebp)

decl %ecx
jmp power_loop_start

end_power:
movl -4(%ebp), %eax 
movl %ebp, %esp
popl %ebp
ret

Ответ 1

TL: DR: используйте gcc -m32.

.code32 не изменяет формат выходного файла, а также то, что определяет режим, в котором будет работать ваша программа. Вам не нужно запускать 32-битный код в режиме 64 бит. .code32 предназначен для сборки "чужого" машинного кода, который может потребоваться в качестве данных, или для экспорта в сегменте общей памяти. Если это не то, что вы делаете, избегайте этого, чтобы получить ошибки времени сборки при построении .S в неправильном режиме, если у него есть какие-либо инструкции push или pop, например.

Предложение: используйте расширение .S для рукописного ассемблера. (gcc foo.S будет запускаться через препроцессор C до as, поэтому вы можете #include заголовок с номерами системного вызова, например). Кроме того, он отличает его от .S вывода компилятора (от gcc foo.c -O3 -S).

Чтобы создать 32-битные двоичные файлы, используйте одну из этих команд

gcc -g foo.S -o foo -m32 -nostdlib -static  # static binary with absolutely no libraries or startup code
                       # -nostdlib by itself makes static executables on Linux, but not OS X.

gcc -g foo.S -o foo -m32                  # dynamic binary including the startup boilerplate code.  Use with code that defines a main() but not a _start

Документация для nostdlib, -nostartfiles и -static.


Использование функций libc из _start (см. конец этого ответа для примера)

Некоторые функции, такие как malloc(3) или stdio-функции, включая printf(3), зависят от инициализации некоторых глобальных данных (например, FILE *stdout и объекта, на который он фактически указывает).

gcc -nostartfiles не содержит код шаблона CRT _start, но все же связывает libc (динамически, по умолчанию). В Linux общие библиотеки могут иметь разделы инициализатора, которые запускаются динамическим компоновщиком, когда он загружает их, прежде чем переходить к вашей точке входа _start. Так что gcc -nostartfiles hello.S по-прежнему позволяет вам звонить printf. Для динамического исполняемого файла ядро ​​запускает /lib/ld-linux.so.2 вместо его непосредственного использования (используйте readelf -a, чтобы увидеть строку "ELF интерпретатор" в вашем двоичном файле). Когда ваш _start в конечном итоге будет запущен, не все регистры будут обнулены, потому что динамический компоновщик выполнил код в вашем процессе.

Однако gcc -nostartfiles -static hello.S свяжется, но сбой во время выполнения, если вы вызываете printf или что-то, не вызывая внутренние функции init glibc. (см. комментарий Майкла Пётча).


Конечно, вы можете поместить любую комбинацию файлов .c, .S и .o в одну и ту же командную строку, чтобы связать их все в один исполняемый файл. Если у вас есть C, не забудьте -Og -Wall -Wextra: вы не хотите отлаживать ваш asm, когда проблема была чем-то простым в C, который вызывает это, о котором компилятор мог предупредить вас.

Используйте -v, чтобы gcc показывал вам команды, которые он запускает для сборки и соединения. Сделать это "вручную" :

as foo.S -o foo.o -g --32 &&      # skips the preprocessor
ld -o foo foo.o  -m elf_i386

file foo
foo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

gcc -nostdlib -m32 легче запомнить и напечатать, чем два разных варианта для as и ld (--32 и -m elf_i386). Кроме того, он работает на всех платформах, включая те, где исполняемый формат не является ELF. ( Но примеры Linux не будут работать в OS X, потому что номера системных вызовов отличаются или в Windows, потому что они даже не используют int 0x80 ABI.)


NASM/Yasm

gcc не может обрабатывать синтаксис NASM. (-masm=intel больше похож на MASM, чем на синтаксис NASM, где вам нужно offset symbol, чтобы получить адрес сразу). И, конечно, директивы различны (например, .globl vs global).

Вы можете построить с помощью nasm или yasm, затем свяжите .o с gcc, как указано выше, или ld напрямую.

Я использую оболочку script, чтобы избежать повторного ввода одного и того же имени файла с тремя разными расширениями. (nasm и yasm по умолчанию - file.asmfile.o, в отличие от GNU в качестве вывода по умолчанию a.out). Используйте это с помощью -m32, чтобы собрать и связать 32-разрядные исполняемые файлы ELF. Не все ОС используют ELF, поэтому этот script менее переносимый, чем использование gcc -nostdlib -m32 для ссылки будет..

#!/bin/sh
# usage: asm-link [-q] [-m32] foo.asm  [assembler options ...]
# Just use a Makefile for anything non-trivial.  This script is intentionally minimal and doesn't handle multiple source files

verbose=1                       # defaults
fmt=-felf64
#ldopt=-melf_i386

while getopts 'm:vq' opt; do
    case "$opt" in
        m)  if [ "m$OPTARG" = "m32" ]; then
                fmt=-felf32
                ldopt=-melf_i386
            fi
            if [ "m$OPTARG" = "mx32" ]; then
                fmt=-felfx32
                ldopt=-melf32_x86_64
            fi
            # default is -m64
            ;;
        q)  verbose=0 ;;
        v)  verbose=1 ;;
    esac
done
shift "$((OPTIND-1))"   # Shift off the options and optional --

src=$1
base=${src%.*}
shift

[ "$verbose" = 1 ] && set -x    # print commands as they're run, like make

#yasm "$fmt" -Worphan-labels -gdwarf2 "$src" "[email protected]" &&
nasm "$fmt" -Worphan-labels -g -Fdwarf "$src" "[email protected]" &&
    ld $ldopt -o "$base" "$base.o"

# yasm -gdwarf2 includes even .local labels so they show up in objdump output
# nasm defaults to that behaviour of including even .local labels

# nasm defaults to STABS debugging format, but -g is not the default

Я предпочитаю yasm по нескольким причинам, в том числе по умолчанию для создания long- nop вместо заполнения со многими однобайтовыми nop s. Это приводит к беспорядочному выводу разборки, а также к замедлению, если nops когда-либо выполняются. (В NASM вам нужно использовать макрос smartalign.)


Пример: программа, использующая функции libc из _start

# hello32.S

#include <asm/unistd_32.h>   // syscall numbers.  only #defines, no C declarations left after CPP to cause asm syntax errors

.text
#.global main   # uncomment these to let this code work as _start, or as main called by glibc _start
#main:
#.weak _start

.global _start
_start:
        mov     $__NR_gettimeofday, %eax  # make a syscall that we can see in strace output so we know when we get here
        int     $0x80

        push    %esp
        push    $print_fmt
        call   printf

        #xor    %ebx,%ebx                 # _exit(0)
        #mov    $__NR_exit_group, %eax    # same as glibc _exit(2) wrapper
        #int    $0x80                     # won't flush the stdio buffer

        movl    $0, (%esp)   # reuse the stack slots we set up for printf, instead of popping
        call    exit         # exit(3) does an fflush and other cleanup

        #add    $8, %esp     # pop the space reserved by the two pushes
        #ret                 # only works in main, not _start

.section .rodata
print_fmt: .asciz "Hello, World!\n%%esp at startup = %#lx\n"

$ gcc -m32 -nostdlib hello32.S
/tmp/ccHNGx24.o: In function `_start':
(.text+0x7): undefined reference to `printf'
...
$ gcc -m32 hello32.S
/tmp/ccQ4SOR8.o: In function `_start':
(.text+0x0): multiple definition of `_start'
...

Сбой во время выполнения, потому что ничего не вызывает функции init glibc. (__libc_init_first, __dl_tls_setup и __libc_csu_init в этом порядке, в соответствии с комментарием Михаила Петча. Существуют и другие реализации libc, включая MUSL, который предназначен для статической компоновки и работает без вызовов инициализации.)

$ gcc -m32 -nostartfiles -static hello32.S     # fails at run-time
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, BuildID[sha1]=ef4b74b1c29618d89ad60dbc6f9517d7cdec3236, not stripped
$ strace -s128 ./a.out
execve("./a.out", ["./a.out"], [/* 70 vars */]) = 0
[ Process PID=29681 runs in 32 bit mode. ]
gettimeofday(NULL, NULL)                = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=0} ---
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

Вы также можете gdb ./a.out и запустите b _start, layout reg, run и посмотрите, что произойдет.


$ gcc -m32 -nostartfiles hello32.S             # Correct command line
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=7b0a731f9b24a77bee41c13ec562ba2a459d91c7, not stripped

$ ./a.out
Hello, World!
%esp at startup = 0xffdf7460

$ ltrace -s128 ./a.out > /dev/null
printf("Hello, World!\n%%esp at startup = %#lx\n", 0xff937510)      = 43    # note the different address: Address-space layout randomization at work
exit(0 <no return ...>
+++ exited (status 0) +++

$ strace -s128 ./a.out > /dev/null        # redirect stdout so we don't see a mix of normal output and trace output
execve("./a.out", ["./a.out"], [/* 70 vars */]) = 0
[ Process PID=29729 runs in 32 bit mode. ]
brk(0)                                  = 0x834e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
....   more syscalls from dynamic linker code
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
mmap2(NULL, 1814236, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff7556000    # map the executable text section of the library
... more stuff
# end of dynamic linker code, finally jumps to our _start

gettimeofday({1461874556, 431117}, NULL) = 0
fstat64(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0  # stdio is figuring out whether stdout is a terminal or not
ioctl(1, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0xff938870) = -1 ENOTTY (Inappropriate ioctl for device)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7743000      # 4k buffer for stdout
write(1, "Hello, World!\n%esp at startup = 0xff938fb0\n", 43) = 43
exit_group(0)                           = ?
+++ exited with 0 +++

Если бы мы использовали _exit(0) или сделали систему sys_exit себя с int 0x80, write(2) не произошло бы. Если stdout перенаправлен на не-tty, по умолчанию он заполняется с полным буфером (не с буферизацией строки), поэтому write(2) запускается только fflush(3) как часть exit(3). Без перенаправления вызов printf(3) со строкой, содержащей символы новой строки, немедленно будет скрыт.

Поведение по-разному в зависимости от того, является ли stdout терминалом, может быть желательным, но только если вы делаете это специально, а не по ошибке.