Ответ 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.asm
→ file.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 терминалом, может быть желательным, но только если вы делаете это специально, а не по ошибке.