Assemblare i binari a 32 bit su un sistema a 64 bit (GNU toolchain)

Scrivo il codice assembly che può essere compilato:

as power.s -o power.o 

c’è un problema quando collego il file object power.o:

 ld power.o -o power 

Per poter funzionare sul sistema operativo a 64 bit (Ubuntu 14.04), ho aggiunto .code32 all’inizio del file power.s , tuttavia ottengo ancora un errore:

Errore di segmentazione (core dumped)

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 

TL: DR : usa gcc -m32 .

.code32 non cambia il formato del file di output, e questo è ciò che determina la modalità in cui verrà eseguito il programma. Sta a te non provare a eseguire codice a 32 bit in modalità 64 bit. .code32 serve per assemblare il codice macchina “estraneo” che si potrebbe desiderare come dati, o per esportare in un segmento di memoria condivisa. Se non è quello che stai facendo, .S in modo che tu possa ottenere errori in fase di compilazione quando costruisci un file .S nella modalità sbagliata se per esempio hai istruzioni push o pop .

Suggerimento: usa l’estensione .S per assemblatore scritto a mano. ( gcc foo.S lo eseguirà attraverso il preprocessore C prima, così puoi #include un’intestazione con numeri syscall, per esempio). Inoltre, lo distingue dall’output del compilatore gcc foo.c -O3 -S (da gcc foo.c -O3 -S ).

Per creare binari a 32 bit, utilizzare uno di questi comandi

 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 

Documentazione per nostdlib , -nostartfiles e -static .


Uso delle funzioni di libc da _start (vedere la fine di questa risposta per un esempio)

Alcune funzioni, come malloc(3) o stdio, incluso printf(3) , dipendono da alcuni dati globali inizializzati (ad es. FILE *stdout e l’object a cui punta effettivamente).

gcc -nostartfiles _start codice boiler _start CRT, ma collega ancora libc (dynamicmente, per impostazione predefinita). Su Linux, le librerie condivise possono avere sezioni di inizializzazione che vengono eseguite dal linker dinamico quando le carica, prima di saltare al punto di ingresso _start . Quindi gcc -nostartfiles hello.S ti consente comunque di chiamare printf . Per un eseguibile dinamico, il kernel esegue /lib/ld-linux.so.2 su di esso invece di eseguirlo direttamente (usa readelf -a per vedere la stringa “interprete ELF” nel tuo binario). Al termine _start , non verranno azzerati tutti i registri, poiché il linker dinamico ha eseguito il codice nel processo.

Tuttavia, gcc -nostartfiles -static hello.S collegherà, ma si gcc -nostartfiles -static hello.S in fase di esecuzione se si chiama printf o qualcosa senza chiamare le funzioni di init interne di glibc. (vedi il commento di Michael Petch).


Naturalmente è ansible inserire qualsiasi combinazione di file .c , .S e .o nella stessa riga di comando per collegarli tutti in un unico file eseguibile. Se si dispone di C, non dimenticare -Og -Wall -Wextra : non si desidera eseguire il debug di asm quando il problema era qualcosa di semplice nella C che lo chiama che il compilatore potrebbe averti avvertito.

Usa -v per fare in modo che gcc ti mostri i comandi che esegue per assemblare e colbind. Per farlo “manualmente” :

 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 è più facile da ricordare e digitare rispetto alle due diverse opzioni per as e ld ( --32 e -m elf_i386 ). Inoltre, funziona su tutte le piattaforms, comprese quelle in cui il formato eseguibile non è ELF. ( Ma gli esempi di Linux non funzioneranno su OS X, perché i numeri di chiamata di sistema sono diversi , o su Windows perché non usa nemmeno l’ int 0x80 .)


NASM / yasm

gcc non può gestire la syntax NASM. ( -masm=intel è più simile alla syntax MASM della NASM, in cui è necessario il offset symbol per ottenere l’indirizzo come immediato). E ovviamente le direttive sono diverse (eg .globl vs global ).

Puoi build con nasm o yasm , quindi colbind il file .o con gcc come sopra o direttamente ld .

Uso uno script wrapper per evitare la ripetizione della digitazione dello stesso nome di file con tre diverse estensioni. (nasm e yasm sono predefi file.asm a file.asm -> file.o , a differenza di GNU come output predefinito di a.out ). Usalo con -m32 per assemblare e colbind i file eseguibili ELF a 32 bit. Non tutti i sistemi operativi utilizzano ELF, quindi questo script è meno portabile rispetto all’utilizzo di gcc -nostdlib -m32 per colbind sarebbe ..

 #!/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 

Io preferisco lo yasm per alcuni motivi, incluso il fatto che per impostazione predefinita è necessario eseguire long-n s s anziché eseguire il padding con molti byte singoli nop . Ciò rende l’output disordinato di sassembly, oltre ad essere più lento se i nops vengono mai eseguiti. (In NASM, devi usare il pacchetto di macro smartalign .)


Esempio: un programma che utilizza le funzioni libc da _start

 # hello32.S #include  // 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' ... 

Non funziona in fase di esecuzione, poiché nulla chiama le funzioni init di glibc. ( __libc_init_first , __dl_tls_setup e __libc_csu_init in questo ordine, secondo il commento di Michael __libc_csu_init implementazioni di libc esistono, incluso MUSL che è progettato per il collegamento statico e funziona senza chiamate di inizializzazione.)

 $ 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) 

Puoi anche gdb ./a.out ed eseguire b _start , layout reg , run e vedere cosa succede.


 $ 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  +++ 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 +++ 

Se avessimo usato _exit(0) o reso il sistema sys_exit chiamarci con int 0x80 , la write(2) non sarebbe avvenuta . Con lo stdout reindirizzato a un non-tty, ha come valore predefinito il buffer completo (non il buffer di riga), quindi la write(2) viene triggersta solo da fflush(3) come parte di exit(3) . Senza reindirizzamento, chiamando printf(3) con una stringa contenente newline si svuoterà immediatamente.

Comportarsi diversamente a seconda che stdout sia un terminale può essere desiderabile, ma solo se lo si fa apposta, non per sbaglio.