GNU/Linux >> Znalost Linux >  >> Linux

Nelze volat funkci standardní knihovny C na 64bitovém Linuxu z kódu sestavení (yasm).

Vaše gcc ve výchozím nastavení vytváří spustitelné soubory PIE (32bitové absolutní adresy již nejsou v x86-64 Linuxu povoleny?).

Nejsem si jistý proč, ale když to uděláte, linker automaticky nevyřeší call puts do call [email protected] . Stále existuje puts Vygenerován záznam PLT, ale call tam nejde.

Za běhu se dynamický linker pokusí vyřešit puts přímo na symbol libc tohoto jména a opravte call rel32 . Symbol je však vzdálen více než +-2^31, takže dostáváme varování před přetečením R_X86_64_PC32 přemístění. Nízkých 32 bitů cílové adresy je správných, ale horních bitů nikoliv. (Takže vaše call skočí na špatnou adresu).

Váš kód mi funguje, pokud sestavuji s gcc -no-pie -fno-pie call-lib.c libcall.o . -no-pie je kritická část:je to možnost linkeru. Váš příkaz YASM se nemusí měnit.

Při vytváření tradičního spustitelného souboru závislého na poloze linker otočí puts symbol pro cíl volání do [email protected] pro vás, protože propojujeme dynamický spustitelný soubor (místo statického propojení knihovny libc s gcc -static -fno-pie , v takovém případě call mohl jít přímo na funkci libc.)

Každopádně to je důvod, proč gcc vysílá call [email protected] (syntaxe GAS) při kompilaci s -fpie (výchozí na ploše, ale ne výchozí na https://godbolt.org/), ale jen call puts při kompilaci s -fno-pie .

Viz Co znamená @plt zde? pro více informací o PLT a také Omlouváme se za stav dynamických knihoven v Linuxu před několika lety. (Moderní gcc -fno-plt je jako jeden z nápadů v tom blogovém příspěvku.)

BTW, přesnější/specifičtější prototyp by umožnil gcc vyhnout se vynulování EAX před voláním foo :

extern void foo(); v C znamená extern void foo(...);
Můžete to deklarovat jako extern void foo(void); , což je () znamená v C++. C++ nepovoluje deklarace funkcí, které ponechávají argumenty nespecifikované.

vylepšení asm

Můžete také zadat message v section .rodata (data pouze pro čtení, propojená jako součást textového segmentu).

Nepotřebujete rám zásobníku, jen něco, co zarovná zásobník o 16 před voláním. Figurína push rax udělá to.

Nebo můžeme zavolat puts skákáním místo volání, se stejnou pozicí zásobníku jako při vstupu do této funkce. Toto funguje s nebo bez PIE. Stačí nahradit call s jmp , pokud RSP ukazuje na vaši vlastní zpáteční adresu.

Pokud chcete vytvořit spustitelné soubory PIE (nebo sdílené knihovny), máte dvě možnosti

  • call puts wrt ..plt - explicitně volat přes PLT.
  • call [rel puts wrt ..got] - explicitně proveďte nepřímé volání prostřednictvím položky GOT, jako je -fno-plt gcc styl code-gen. (Použití režimu adresování relativního RIP k dosažení GOT, proto rel klíčové slovo).

WRT =S úctou. Návod NASM dokumentuje wrt ..plt a viz také část 7.9.3:Speciální symboly a WRT.

Normálně byste použili default rel v horní části souboru, takže můžete skutečně použít call [puts wrt ..got] a přesto získat režim adresování relativní k RIP. V kódu PIE nebo PIC nemůžete použít 32bitový režim absolutního adresování.

call [puts wrt ..got] sestaví do paměti nepřímého volání pomocí ukazatele funkce, které dynamické propojení je uloženo v GOT. (Včasná vazba, ne líné dynamické propojení.)

Dokumenty NASM ..got pro získání adresy proměnných v sekci 9.2.3. Funkce v (ostatních) knihovnách jsou identické:namísto přímého volání získáte ukazatel z GOT, protože offset není konstanta linkového času a nemusí se vejít do 32bitů.

YASM také přijímá call [puts wrt ..GOTPCREL] , jako je syntaxe AT&T call *[email protected](%rip) , ale NASM nikoli.

; 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

V pozici-závislá spustitelný, můžete použít mov edi, message místo RIP-relativní LEA. Má menší velikost kódu a může běžet na více prováděcích portech na většině CPU.

Ve spustitelném souboru bez PIE můžete také použít call puts nebo jmp puts a nechte linker, aby to vyřešil, pokud nechcete efektivnější dynamické propojení ve stylu no-plt. Ale pokud se rozhodnete pro statické propojení libc, myslím, že toto je jediný způsob, jak získáte přímý jmp na funkci libc.

(Myslím, že možnost statického propojení pro non-PIE je proč ld je ochoten automaticky generovat PLT stub pro non-PIE, ale ne pro PIE nebo sdílené knihovny. Vyžaduje, abyste řekli, co máte na mysli, když propojujete sdílené objekty ELF.)

Pokud jste použili call puts v PIE (call rel32 ), mohlo by to fungovat, pouze pokud byste staticky propojili implementaci puts nezávislou na pozici do vašeho PIE, takže celá věc byl jeden spustitelný soubor, který se za běhu načetl na náhodnou adresu (obvyklým mechanismem dynamického linkeru), ale jednoduše nebyl závislý na libc.so.6


0xe8 po operačním kódu následuje podepsaný offset, který se aplikuje na PC (který do té doby postoupil k další instrukci) pro výpočet cíle větve. Proto objdump interpretuje cíl větve jako 0x671 .

YASM vykresluje nuly, protože pravděpodobně přemístil tento offset, což je způsob, jakým žádá zavaděč, aby naplnil správný offset pro puts během načítání. Zavaděč při výpočtu přemístění naráží na přetečení, což může znamenat, že puts je ve větším offsetu od vašeho hovoru, než může být reprezentováno 32bitovým offsetem se znaménkem. Zavaděč proto tuto instrukci neopraví a dojde k havárii.

66c: e8 00 00 00 00 zobrazuje neobsazenou adresu. Pokud se podíváte do tabulky přemístění, měli byste vidět přemístění na 0x66d . Není neobvyklé, že assembler vyplní adresy/offsety relokacemi jako všechny nuly.

Tato stránka naznačuje, že YASM má WRT direktiva, která může řídit použití .got , .plt , atd.

Podle S9.2.5 v dokumentaci NASM to vypadá, že můžete použít CALL puts WRT ..plt (za předpokladu, že YASM má stejnou syntaxi).


Linux
  1. Tabulka systémových volání Linux nebo cheatsheet pro shromáždění

  2. Jak zavolat Wine dll z pythonu na Linuxu?

  3. Může exit() selhat při ukončení procesu?

  1. Jak může sdílená knihovna (.so) volat funkci, která je implementována v jejím načítacím programu?

  2. Přesměrování výstupu z funkčního bloku do souboru v Linuxu

  3. Jak mohu odstranit jenkins úplně z linuxu

  1. Jak volat funkci C v C++, funkci C++ v C (Mix C a C++)

  2. Mohu zavést Linux z VHD?

  3. Jak mohu snadno převést speciální entity HTML ze standardního vstupního proudu v Linuxu?