GNU/Linux >> Linux Esercitazione >  >> Linux

Impossibile chiamare la funzione della libreria standard C su Linux a 64 bit dal codice assembly (yasm).

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 il rel 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).


Linux
  1. Come chiamare Wine dll da Python su Linux?

  2. Come caricare i moduli del kernel Linux dal codice C?

  3. exit() può non riuscire a terminare il processo?

  4. Come può una libreria condivisa (.so) chiamare una funzione implementata nel suo programma di caricamento?

  5. Reindirizzamento dell'output da un blocco funzione a un file in Linux

Nozioni di base sulla compilazione di software dal codice sorgente in Linux

Come ottenere notizie istantaneamente dalla riga di comando in Linux

Come posso profilare il codice C++ in esecuzione su Linux?

Posso avviare Linux da un VHD?

Chiama una chiamata di sistema Linux da un linguaggio di scripting

Come posso convertire facilmente entità speciali HTML da un flusso di input standard in Linux?