Сборка 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
2 ответа
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
оставляет ЭЛТ _start
стандартный код, но все же ссылки libc
(динамически, по умолчанию). В Linux у разделяемых библиотек могут быть разделы инициализатора, которые запускаются динамическим компоновщиком, когда он загружает их, прежде чем перейти к вашему _start
точка входа. Так gcc -nostartfiles hello.S
все еще позволяет тебе звонить printf
, Для динамического исполняемого файла ядро запускается /lib/ld-linux.so.2
на нем вместо того, чтобы запустить его напрямую (используйте readelf -a
чтобы увидеть строку "ELF интерпретатор" в вашем двоичном файле). Когда ваш _start
в конце концов, не все регистры будут обнулены, потому что динамический компоновщик запускает код в вашем процессе.
Тем не мение, gcc -nostartfiles -static hello.S
будет ссылаться, но сбой во время выполнения, если вы звоните printf
или что-то без вызова внутренних функций инициализации 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
против global
).
Вы можете построить с nasm
или же yasm
затем свяжите .o
с gcc
как указано выше, или ld
непосредственно.
Я использую скрипт-обертку, чтобы избежать повторного ввода одного и того же имени файла с тремя разными расширениями. (NASM и ЯСМ по умолчанию file.asm
-> file.o
в отличие от GNU как вывод по умолчанию a.out
). Используйте это с -m32
собрать и связать 32-битные исполняемые файлы ELF. Не все ОС используют ELF, поэтому этот скрипт менее переносим, чем 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" "$@" &&
nasm "$fmt" -Worphan-labels -g -Fdwarf "$src" "$@" &&
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
Я предпочитаю язм по нескольким причинам, включая то, что по умолчанию nop
s вместо заполнения со многими однобайтовыми 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's _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'
...
Сбой во время выполнения, потому что ничто не вызывает функции инициализации 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's 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 терминалом, может быть желательным, но только если вы делаете это нарочно, а не по ошибке.
Я изучаю сборку x86 (на 64-битной Ubuntu 18.04) и у меня была похожая проблема с точно таким же примером (это из " Программирование с нуля", в главе 4 [ http://savannah.nongnu.org/projects/pgubook/ ]).
После осмотра я нашел следующие две строки и связал это:
as power.s -o power.o --32
ld power.o -o power -m elf_i386
Они сообщают компьютеру, что вы работаете только в 32-разрядной версии (несмотря на 64-разрядную архитектуру).
Если вы хотите использовать gdb debugging
, затем используйте строку ассемблера:
as --gstabs power.s -o power.o --32.
.Code32 кажется ненужным.
Я не пробовал по-твоему, но ассемблер GNU (газ), кажется, хорошо с:
.глобл старт
# (то есть, нет "а" в глобальном).
Более того, я бы предположил, что вы, вероятно, захотите оставить комментарии в исходном коде, так как кажется, что рекомендуется много комментировать в сборке. (Даже если вы единственный, кто просматривает код, вам будет проще понять, что вы делали, если вы посмотрите на это несколько месяцев или лет спустя.)
Было бы неплохо знать, как изменить это, чтобы использовать 64-bit R*X
а также RBP
, RSP
регистрируется хотя.