Il tuo gcc sta creando eseguibili PIE per impostazione predefinita (gli indirizzi assoluti a 32 bit non sono più consentiti in Linux x86-64?).
Non sono sicuro del perché, ma quando lo fai il linker non risolve automaticamente call puts
a call [email protected]
. C'è ancora un puts
Voce PLT generata, ma call
non va lì.
In fase di esecuzione, il linker dinamico tenta di risolvere puts
direttamente al simbolo libc con quel nome e correggi il call rel32
. Ma il simbolo è a più di +-2^31 di distanza, quindi riceviamo un avviso sull'overflow del R_X86_64_PC32
trasferimento. I 32 bit bassi dell'indirizzo di destinazione sono corretti, ma i bit superiori no. (Quindi il tuo call
passa a un indirizzo sbagliato).
Il tuo codice funziona per me se creo con gcc -no-pie -fno-pie call-lib.c libcall.o
. Il -no-pie
è la parte critica:è l'opzione del linker. Il tuo comando YASM non deve cambiare.
Quando si crea un eseguibile tradizionale dipendente dalla posizione, il linker trasforma il puts
simbolo per l'obiettivo della chiamata in [email protected]
per te, perché stiamo collegando un eseguibile dinamico (invece di collegare staticamente libc con gcc -static -fno-pie
, nel qual caso call
potrebbe andare direttamente alla funzione libc.)
Ad ogni modo, questo è il motivo per cui gcc emette call [email protected]
(sintassi GAS) durante la compilazione con -fpie
(l'impostazione predefinita sul desktop, ma non l'impostazione predefinita su https://godbolt.org/), ma solo call puts
durante la compilazione con -fno-pie
.
Vedi Cosa significa @plt qui? per ulteriori informazioni sul PLT, e anche Spiacente stato delle librerie dinamiche su Linux di alcuni anni fa. (Il moderno gcc -fno-plt
è come una delle idee in quel post del blog.)
A proposito, un prototipo più preciso/specifico consentirebbe a gcc di evitare di azzerare EAX prima di chiamare foo
:
extern void foo();
in C significa extern void foo(...);
Potresti dichiararlo come extern void foo(void);
, che è ciò che ()
significa in C++. C++ non consente dichiarazioni di funzione che lasciano gli argomenti non specificati.
miglioramenti asm
Puoi anche inserire message
in section .rodata
(dati di sola lettura, collegati come parte del segmento di testo).
Non hai bisogno di uno stack frame, solo qualcosa per allineare lo stack di 16 prima di una chiamata. Un push rax
fittizio lo farà.
Oppure possiamo chiamare in coda puts
saltando ad esso invece di chiamarlo, con la stessa posizione nello stack come all'ingresso di questa funzione. Funziona con o senza PIE. Basta sostituire call
con jmp
, purché RSP punti al tuo indirizzo di ritorno.
Se vuoi creare eseguibili PIE (o librerie condivise), hai due opzioni
call puts wrt ..plt
- chiamata esplicita attraverso il PLT.call [rel puts wrt ..got]
- eseguire esplicitamente una chiamata indiretta attraverso la voce GOT, come-fno-plt
di gcc stile di code-gen. (Utilizzando una modalità di indirizzamento relativa al RIP per raggiungere il GOT, da cui ilrel
parola chiave).
WRT =Rispetto a. Il manuale NASM documenta wrt ..plt
e vedere anche la sezione 7.9.3:simboli speciali e WRT.
Normalmente useresti default rel
nella parte superiore del file in modo da poter utilizzare effettivamente call [puts wrt ..got]
e ottenere comunque una modalità di indirizzamento relativa a RIP. Non puoi utilizzare una modalità di indirizzamento assoluto a 32 bit nel codice PIE o PIC.
call [puts wrt ..got]
si assembla in una chiamata indiretta dalla memoria utilizzando il puntatore a funzione che il collegamento dinamico memorizzato nel GOT. (Collegamento dinamico anticipato, non pigro.)
Documenti NASM ..got
per ottenere l'indirizzo delle variabili nella sezione 9.2.3. Le funzioni nelle (altre) librerie sono identiche:ottieni un puntatore dal GOT invece di chiamare direttamente, perché l'offset non è una costante del tempo di collegamento e potrebbe non rientrare in 32 bit.
YASM accetta anche call [puts wrt ..GOTPCREL]
, come la sintassi AT&T call *[email protected](%rip)
, ma NASM no.
; don't use BITS 64. You *want* an error if you try to assemble this into a 32-bit .o
default rel ; RIP-relative addressing instead of 32-bit absolute by default; makes the [rel ...] optional
section .rodata ; .rodata is best for constants, not .data
message:
db 'foo() called', 0
section .text
global foo
foo:
sub rsp, 8 ; align the stack by 16
; PIE with PLT
lea rdi, [rel message] ; needed for PIE
call puts WRT ..plt ; tailcall puts
;or
; PIE with -fno-plt style code, skips the PLT indirection
lea rdi, [rel message]
call [rel puts wrt ..got]
;or
; non-PIE
mov edi, message ; more efficient, but only works in non-PIE / non-PIC
call puts ; linker will rewrite it into call [email protected]
add rsp,8 ; remove the padding
ret
In una posizione-dipendente eseguibile, puoi usare mov edi, message
invece di un LEA relativo al RIP. Ha una dimensione di codice più piccola e può essere eseguito su più porte di esecuzione sulla maggior parte delle CPU.
In un eseguibile non-PIE, potresti anche usare call puts
o jmp puts
e lascia che il linker lo risolva, a meno che tu non voglia un collegamento dinamico più efficiente in stile no-plt. Ma se scegli di collegare staticamente libc, penso che questo sia l'unico modo per ottenere un jmp diretto alla funzione libc.
(Penso che la possibilità di collegamento statico per non-PIE sia perché ld
è disposto a generare stub PLT automaticamente per non-PIE, ma non per PIE o librerie condivise. Ti richiede di dire cosa intendi quando colleghi oggetti condivisi ELF.)
Se hai usato call puts
in un PIE (call rel32
), potrebbe funzionare solo se hai collegato staticamente un'implementazione indipendente dalla posizione di puts
nella tua torta, quindi l'intera cosa era un eseguibile che veniva caricato a un indirizzo casuale in fase di esecuzione (tramite il solito meccanismo di linker dinamico), ma semplicemente non aveva una dipendenza da libc.so.6
Il 0xe8
opcode è seguito da un offset con segno da applicare al PC (che a quel punto è avanzato all'istruzione successiva) per calcolare l'obiettivo del ramo. Quindi objdump
sta interpretando la destinazione del ramo come 0x671
.
YASM rende gli zeri perché probabilmente ha inserito un riposizionamento su quell'offset, che è il modo in cui chiede al caricatore di popolare l'offset corretto per puts
durante il caricamento. Il caricatore sta riscontrando un overflow durante il calcolo del riposizionamento, il che potrebbe indicare che puts
si trova a un ulteriore offset dalla chiamata rispetto a quello che può essere rappresentato in un offset con segno a 32 bit. Quindi il caricatore non riesce a correggere questa istruzione e si verifica un arresto anomalo.
66c: e8 00 00 00 00
mostra l'indirizzo non popolato. Se guardi nella tabella dei trasferimenti, dovresti vedere un trasferimento su 0x66d
. Non è raro che l'assembler compili indirizzi/offset con rilocazioni come tutti zeri.
Questa pagina suggerisce che YASM ha un WRT
direttiva che può controllare l'uso di .got
, .plt
, ecc.
Per S9.2.5 sulla documentazione NASM, sembra che tu possa usare CALL puts WRT ..plt
(presumendo che YASM abbia la stessa sintassi).