Ihr gcc erstellt standardmäßig ausführbare PIE-Dateien (absolute 32-Bit-Adressen sind in x86-64-Linux nicht mehr zulässig?).
Ich bin mir nicht sicher warum, aber dabei löst der Linker call puts
nicht automatisch auf bis call [email protected]
. Es gibt noch eine puts
PLT-Eintrag generiert, aber der call
geht nicht dorthin.
Zur Laufzeit versucht der dynamische Linker, puts
aufzulösen direkt zum libc-Symbol dieses Namens und korrigieren Sie den call rel32
. Aber das Symbol ist mehr als +-2^31 entfernt, also erhalten wir eine Warnung über den Überlauf von R_X86_64_PC32
Verlegung. Die unteren 32 Bits der Zieladresse sind korrekt, die oberen Bits jedoch nicht. (Also Ihr call
springt zu einer falschen Adresse).
Ihr Code funktioniert für mich, wenn ich mit gcc -no-pie -fno-pie call-lib.c libcall.o
baue . Die -no-pie
ist der kritische Teil:Es ist die Linker-Option. Ihr YASM-Befehl muss nicht geändert werden.
Beim Erstellen einer herkömmlichen positionsabhängigen ausführbaren Datei dreht der Linker den puts
Symbol für das Anrufziel in [email protected]
für Sie, da wir eine dynamische ausführbare Datei verknüpfen (anstatt libc statisch mit gcc -static -fno-pie
zu verknüpfen , in diesem Fall call
könnte direkt gehen zur libc-Funktion.)
Wie auch immer, das ist der Grund, warum gcc call [email protected]
ausgibt (GAS-Syntax) beim Kompilieren mit -fpie
(der Standard auf Ihrem Desktop, aber nicht der Standard auf https://godbolt.org/), sondern nur call puts
beim Kompilieren mit -fno-pie
.
Siehe Was bedeutet @plt hier? für mehr über PLT und auch Entschuldigung für den Zustand dynamischer Bibliotheken unter Linux von vor einigen Jahren. (Der moderne gcc -fno-plt
ist wie eine der Ideen in diesem Blogbeitrag.)
Übrigens, ein genauerer/spezifischerer Prototyp würde gcc vermeiden lassen, EAX auf Null zu setzen, bevor foo
aufgerufen wird :
extern void foo();
in C bedeutet extern void foo(...);
Sie könnten es als extern void foo(void);
deklarieren , was ()
ist bedeutet in C++. C++ erlaubt keine Funktionsdeklarationen, die die Argumente unspezifiziert lassen.
asm-Verbesserungen
Sie können auch message
eingeben in section .rodata
(schreibgeschützte Daten, verlinkt als Teil des Textsegments).
Sie brauchen keinen Stapelrahmen, nur etwas, um den Stapel vor einem Anruf um 16 auszurichten. Ein Dummy push rax
werde es tun.
Oder wir können puts
per Tail-Call aufrufen durch Springen zu ihr, anstatt sie aufzurufen, mit derselben Stapelposition wie beim Eintritt in diese Funktion. Dies funktioniert mit oder ohne PIE. Ersetzen Sie einfach call
mit jmp
, solange RSP auf Ihre eigene Absenderadresse zeigt.
Wenn Sie ausführbare PIE-Dateien (oder gemeinsam genutzte Bibliotheken) erstellen möchten, haben Sie zwei Möglichkeiten
call puts wrt ..plt
- explizit über die PLT aufrufen.call [rel puts wrt ..got]
- Führen Sie explizit einen indirekten Aufruf durch den GOT-Eintrag durch, wie-fno-plt
von gcc Stil der Code-Generierung. (Verwenden eines RIP-relativen Adressierungsmodus, um das GOT zu erreichen, daher derrel
Schlüsselwort).
WRT =In Bezug auf. Das NASM-Handbuch dokumentiert wrt ..plt
, und siehe auch Abschnitt 7.9.3:Sonderzeichen und WRT.
Normalerweise würden Sie default rel
verwenden am Anfang Ihrer Datei, damit Sie call [puts wrt ..got]
tatsächlich verwenden können und trotzdem einen RIP-relativen Adressierungsmodus erhalten. Sie können keinen absoluten 32-Bit-Adressierungsmodus in PIE- oder PIC-Code verwenden.
call [puts wrt ..got]
Zusammenbau zu einem speicherindirekten Aufruf unter Verwendung des Funktionszeigers, der im GOT gespeichert ist. (Frühes Binden, nicht faules dynamisches Verlinken.)
NASM-Dokumente ..got
zum Abrufen der Adresse von Variablen in Abschnitt 9.2.3. Funktionen in (anderen) Bibliotheken sind identisch:Sie erhalten einen Zeiger von der GOT, anstatt sie direkt aufzurufen, da der Offset keine Linkzeitkonstante ist und möglicherweise nicht in 32-Bit passt.
YASM akzeptiert auch call [puts wrt ..GOTPCREL]
, wie die AT&T-Syntax call *[email protected](%rip)
, aber NASM nicht.
; 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 einer Position - abhängig ausführbar ist, können Sie mov edi, message
verwenden anstelle eines RIP-relativen LEA. Es hat eine kleinere Codegröße und kann auf den meisten CPUs auf mehr Ausführungsports ausgeführt werden.
In einer ausführbaren Nicht-PIE-Datei können Sie auch call puts
verwenden oder jmp puts
und lassen Sie es den Linker regeln, es sei denn, Sie möchten eine effizientere dynamische Verknüpfung im No-PLT-Stil. Aber wenn Sie sich dafür entscheiden, libc statisch zu linken, denke ich, dass dies der einzige Weg ist, wie Sie einen direkten jmp zur libc-Funktion bekommen.
(Ich denke, die Möglichkeit der statischen Verlinkung für Nicht-PIE ist warum ld
ist bereit, PLT-Stubs automatisch für Nicht-PIE zu generieren, aber nicht für PIE oder gemeinsam genutzte Bibliotheken. Sie müssen sagen, was Sie meinen, wenn Sie gemeinsame ELF-Objekte verknüpfen.)
Wenn Sie call puts
verwendet haben in einem PIE (call rel32
), könnte es nur funktionieren, wenn Sie eine positionsunabhängige Implementierung von puts
statisch verknüpfen in Ihre PIE, also war das Ganze eine ausführbare Datei, die zur Laufzeit an einer zufälligen Adresse geladen wurde (durch den üblichen dynamischen Linker-Mechanismus), aber einfach keine Abhängigkeit von libc.so.6
hatte
Der 0xe8
Auf opcode folgt ein vorzeichenbehafteter Offset, der an den PC (der zu diesem Zeitpunkt zum nächsten Befehl vorgerückt ist) angelegt wird, um das Verzweigungsziel zu berechnen. Daher objdump
interpretiert das Verzweigungsziel als 0x671
.
YASM gibt Nullen wieder, weil es wahrscheinlich eine Verschiebung an diesem Offset vorgenommen hat, und fordert so den Loader auf, den korrekten Offset für puts
zu füllen während des Ladens. Der Loader stellt beim Berechnen der Verschiebung einen Überlauf fest, was darauf hindeuten kann, dass puts
befindet sich in einem weiteren Offset von Ihrem Aufruf, als in einem 32-Bit-Offset mit Vorzeichen dargestellt werden kann. Daher kann der Loader diese Anweisung nicht beheben und Sie erhalten einen Absturz.
66c: e8 00 00 00 00
zeigt die unbesetzte Adresse. Wenn Sie in Ihrer Umzugstabelle nachsehen, sollten Sie einen Umzug auf 0x66d
sehen . Es ist nicht ungewöhnlich, dass der Assembler Adressen/Offsets mit Verschiebungen als reine Nullen füllt.
Diese Seite schlägt vor, dass YASM einen WRT
hat Direktive, die die Verwendung von .got
steuern kann , .plt
usw.
Gemäß S9.2.5 der NASM-Dokumentation sieht es so aus, als könnten Sie CALL puts WRT ..plt
verwenden (vorausgesetzt, YASM hat die gleiche Syntax).