Eine kurze Analyse des Funktionsaufrufprozesses unter der ARM-Architektur

Eine kurze Analyse des Funktionsaufrufprozesses unter der ARM-Architektur

1. Hintergrundwissen

1. Einführung in ARM64-Register

2. Detaillierte Erklärung der STP-Anweisung (ARMV8-Handbuch)

Schauen wir uns zunächst das Befehlsformat (64 Bit) und den Einfluss der Befehle auf die Ausführungsergebnisse der Registermaschine an.

Typ 1, STP <Xt1>, <Xt2>, [<Xn|SP>],#<imm>

Speichern Sie Xt1 und Xt2 im Adressspeicher, der Xn | SP entspricht, und ändern Sie dann die Adresse von Xn | SP in die neue Adresse von Xn | SP + imm-Offset

Typ 2, STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!

Speichern Sie Xt1 und Xt2 im Adressspeicher, der der Adresse von Xn | SP plus imm entspricht, und ändern Sie dann die Adresse von Xn | SP nach dem Offset von Xn | SP + imm in die neue Adresse.

Typ 3, STP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]

Speichern Sie Xt1 und Xt2 im Adressspeicher, der der Adresse von Xn|SP plus imm entspricht

Im Handbuch gibt es drei Arten von Opcodes, und wir besprechen nur die letzten beiden, die im Programm vorkommen.

Der Pseudocode lautet wie folgt:

Gemeinsame Dekodierung für alle Kodierungen
Ganzzahl n = UInt(Rn);
Ganzzahl t = UInt(Rt);
Ganzzahl t2 = UInt(Rt2);
wenn L:opc<0> == '01' || opc == '11' dann UNDEFINIERT;
Ganzzahlskala = 2 + UInt(opc<1>);
ganzzahlige Datengröße = 8 << Skalierung;
Bits (64) Offset = LSL (SignExtend (imm7, 64), Skalierung);
Boolescher Wert tag_checked = wback || n != 31;
Betrieb für alle Kodierungen
Bits(64)-Adresse;
Bits (Datengröße) Daten1;
Bits (Datengröße) Daten2;
Konstante Ganzzahl dbytes = Datengröße DIV 8;
Boolescher Wert rt_unbekannt = FALSCH;
wenn HaveMTEExt() dann
         SetNotTagCheckedInstruction(!tag_checked);
wenn wback && (t == n || t2 == n) && n != 31 dann
    Einschränkung c = ConstrainUnpredictable();
    behaupten c IN {Constraint_NONE, Constraint_UNKNOWN, Constraint_UNDEF, Constraint_NOP};
    Fall c von
        wenn Constraint_NONE rt_unknown = FALSE; // gespeicherter Wert ist vor dem Rückschreiben
        wenn Constraint_UNKNOWN rt_unknown = TRUE; // gespeicherter Wert ist UNKNOWN
        wenn Constraint_UNDEF UNDEFINED;
        wenn Constraint_NOP EndOfInstruction();
wenn n == 31 dann
 Überprüfen Sie die SPA-Ausrichtung ();
    Adresse = SP[];
anders
    Adresse = X[n];
wenn !postindex dann
    Adresse = Adresse + Offset;
wenn rt_unknown && t == n dann
    Daten1 = Bits (Datengröße) UNBEKANNT;
anders
    Daten1 = X[t];
wenn rt_unknown && t2 == n dann
    Daten2 = Bits (Datengröße) UNBEKANNT;
anders
    Daten2 = X[t2];
Mem[Adresse, dbytes, AccType_NORMAL] = Daten1;
Mem[Adresse+Dbytes, Dbytes, AccType_NORMAL] = Daten2;
wenn wir damals
  wenn Postindex dann
        Adresse = Adresse + Offset;
    wenn n == 31 dann
        SP[] = Adresse;
    anders
        X[n] = Adresse;

Der rote Teil entspricht der Schlüssellogik des Stapelschiebens. Die Bedeutung anderer Montageanweisungen finden Sie im armv8-Handbuch oder bei Baidu.

2. Ein Beispiel

Nachdem wir nun mit den oben genannten Teilen vertraut sind, schauen wir uns ein Beispiel an:

Der C-Code lautet wie folgt:

Die Demontage mehrerer verwandter Funktionen erfolgt wie folgt (normalerweise gibt es nur zwei Anweisungen im Zusammenhang mit dem Stapel-Push):

Haupt\f3\f4\strlen 

Nach dem Durchlaufen von gdb können wir sehen, dass strlen SEGFAULT auslöst, was dazu führt, dass der Prozess hängen bleibt

Nachdem der obige Code kompiliert wurde, gibt es keinen Streifen, sodass die Elf-Datei Symbole enthält

Überprüfen Sie den Betriebsstatus (Inforegister): Achten Sie auf die vier Register $29, $30, SP und PC

Eine Kernidee: Die CPU führt Anweisungen und keinen C-Code aus, und Funktionsaufrufe und -rückgaben sind tatsächlich der Prozess des Pushens und Poppens des Thread-Stapels.

Als nächstes schauen wir uns an, wie die obige Aufrufbeziehung im aktuellen Aufgabenstapel funktioniert:

Die Beziehung zwischen Funktionsaufrufen im Stapel (Funktionsaufruf verschiebt den Stapel, die Adresse verringert sich; Rückgabe verschiebt den Stapel, die Adresse erhöht sich):

Nachfolgend sehen Sie den Vorgang des Stapelschiebens (Hervorhebung)

Werfen wir einen Blick zurück auf die vorherige Zusammenstellung:

Haupt\f3\f4\strlen 

Ausgehend vom aktuellen sp ist Frame 0 strlen und der Stapel ist nicht geöffnet, sodass die aufrufende Funktion der vorherigen Ebene immer noch x30 ist. Daraus lässt sich schließen, dass Frame 1 f3 aufruft

Die Starteintragsassemblierung der Funktion f3:

(gdb) x/2i f3
   0x400600 <f3>: stp x29, x30, [sp,#-48]!
   0x400604 <f3+4>: mov x29, sp

Es ist ersichtlich, dass der von der Funktion f3 geöffnete Stapelspeicher 48 Byte beträgt. Daher ist die Oberseite des Stapels von Frame2 der aktuelle SP + 48 Byte: 0xffffffffff2c0

(gdb) x/gx 0xfffffffff2c0+8
0xffffffffff2c8: 0x000000000040065c
(gdb) x/i 0x000000000040065c
   0x40065c <f4+36>: mov w0, #0x0 // #0
Die Funktion von Frame2 ist sp+8: 0x000000000040065c -> <f4+36>

Weiter mit dem Zurückschieben der Funktion von Frame1 von sp = 0xffffffffff2c0

Die Starteintragsassemblierung der Funktion f4 ist:

(gdb) x/2i f4
   0x400638 <f4>: stp x29, x30, [sp,#-48]!
   0x40063c <f4+4>: mov x29, sp

Es ist ersichtlich, dass der von der Funktion f4 geöffnete Stapelspeicher ebenfalls 48 Byte beträgt. Daher ist die Oberseite des Stapels von Frame3 das aktuelle 0xffffffffff2c0 + 48 Byte: 0xffffffffff2f0

Die Funktion von Frame2 ist 0xffffffff2c0 + 8: 0x000000000040065c -> <f4+36>
(gdb) x/gx 0xfffffffff2f0+8
0xffffffffff2f8: 0x0000000000400684
(gdb) x/i 0x0000000000400684
   0x400684 <main+28>: mov w0, #0x0 // #0

Daher ist die Funktion von Frame3 die Hauptfunktion, und die Spitze des Stapels, die der Hauptfunktion entspricht, ist 0xffffffffff320

Damit ist die Ableitung abgeschlossen (Interessierte können die Ableitung fortsetzen und sehen, wie libc main startet).

Zusammenfassen:

Der Schlüssel zum Push-Stack:

  • Aktuelle Szene
  • Vertraut mit der Stapelöffnungsmethode der CPU-Architektur

3. Praktische Erklärung

Der folgende Kern ist in der Szene verfügbar: Wie Sie sehen, können alle Symbole nicht gefunden werden. Auch nach dem Laden der Symboltabelle funktioniert es immer noch nicht und der eigentliche Aufrufstapel kann nicht analysiert werden.

(gdb) bt
#0 0x0000ffffaeb067bc in ?? () von /lib64/libc.so.6
#1 0x0000aaaad15cf000 in ?? ()
Backtrace gestoppt: vorheriges Frame innerhalb dieses Frames (Stapel beschädigt?)

Schauen Sie sich zunächst das Inforegister an und achten Sie auf die Werte der vier Register x29, x30, sp und pc

Abgeleiteter Aufgabenstapel:

Exportieren Sie zuerst den SP-Inhalt:

Die folgende Abbildung zeigt das tatsächliche Ergebnis. Lassen Sie uns im Detail beschreiben, wie man es ableitet.

pc stellt den aktuell ausgeführten Funktionsbefehl dar. Wenn der aktuelle Befehl nicht geöffnet ist, stellt x30 im Allgemeinen den nächsten Befehl des vorherigen Frames dar, der die aktuelle Funktion aufruft. Wenn man sich die Assembly ansieht, kann sie in die folgende Funktion umgewandelt werden

(gdb) x/i 0xaaaacd3de4fc
   0xaaaacd3de4fc <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+108>: mov x27, x0

Nachdem Sie die oberste Funktion des Stapels gefunden haben, überprüfen Sie den Stapelbetrieb der Funktion:

(gdb) x/6i PGXCNodeConnStr
   0xaaaacd3de490 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)>: sub sp, sp, #0xd0
   0xaaaacd3de494 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+4>: stp x29, x30, [sp,#80]
   0xaaaacd3de498 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+8>: füge x29, sp, #0x50 hinzu

Es ist ersichtlich, dass der vorherige Frame beim aktuellen sp + 0xd0 - 0x80 vorhanden ist, also 0xfffec4cebd40 + 0xd0 - 0x80 = 0xfffec4cebd90, und dass sich der untere Teil des Stapels bei 0xfffec4cebd40 + 0xd0 = 0xfffec4cebe10 befindet.

Daher finden wir die Oberseite des Stapels, die dem Rahmen der nächsten Ebene und der LR-Rückgabeanweisung der vorherigen Ebene entspricht. Durch Umkehren können wir die Funktion build_node_conn_str erhalten

(gdb) x/i 0x0000aaaacd414e08
   0xaaaacd414e08 <build_node_conn_str(Oid, DatabasePool*)+224>: mov x21, x0

Wenn wir die obige Ableitung wiederholen, können wir sehen, dass die Funktion build_node_conn_str einen 176-Byte-Stapel öffnet.

(gdb) x/4i build_node_conn_str
   0xaaaacd414d28 <build_node_conn_str(Oid, DatabasePool*)>: stp x29, x30, [sp,#-176]!
   0xaaaacd414d2c <build_node_conn_str(Oid, DatabasePool*)+4>: mov x29, sp

Also weiter mit 0xfffec4cebe10 + 176 = 0xfffec4cebec0

Überprüfen Sie den Anrufer 0xfffec4cebe10+8 für reload_database_pools

Weiter zu reload_database_pools

(gdb) x/8i Datenbankpools neu laden
   0xaaaacd4225e8 <reload_database_pools(PoolAgent*)>: sub sp, sp, #0x1c0
   0xaaaacd4225ec <reload_database_pools(PoolAgent*)+4>: adrp x5, 0xaaaad15cf000
   0xaaaacd4225f0 <reload_database_pools(PoolAgent*)+8>: adrp x3, 0xaaaacf0ed000
   0xaaaacd4225f4 <reload_database_pools(PoolAgent*)+12>: adrp x4, 0xaaaaceeed000 <_ZN4llvm18ConvertUTF8toUTF16EPPKhS1_PPtS3_NS_15ConversionFlagsE>
   0xaaaacd4225f8 <reload_database_pools(PoolAgent*)+16>: füge x3, x3, #0x9e0 hinzu
   0xaaaacd4225fc <reload_database_pools(PoolAgent*)+20>: adrp x1, 0xaaaacf0ee000 <_ZZ25PoolManagerGetConnectionsP4ListS0_E8__func__+24>
   0xaaaacd422600 <reload_database_pools(PoolAgent*)+24>: stp x29, x30, [sp,#-96]!

Der eigentliche Stapel wird bei 0x220 Bytes geöffnet, daher ist der Stapelboden dieses Frames 0xfffec4cebec0 + 0x220 = 0xfffec4cec0e0

Daher ist die Struktur der grundlegenden Anrufbeziehung wie folgt

Das Obige reicht grundsätzlich aus, um das Problem zu analysieren, sodass keine weiteren Ableitungen erforderlich sind

TIPPS: Diese Anweisung wird im Allgemeinen bei Aufrufen unter der ARM-Architektur verwendet.

stp x29, x30, [sp,#immediate]! mit oder ohne Ausrufezeichen

Daher speichert jede Frame-Ebene die Stapel-Top-Adresse und den LR-Befehl der vorherigen Frame-Ebene. Durch genaues Auffinden des Stapel-Tops des unteren Frames 0 können alle Aufrufbeziehungen schnell abgeleitet werden (der Teil, der von roten gestrichelten Kreisen umkreist ist). Die umgekehrte Lösung der Funktion hängt von der Symboltabelle ab. Solange das Symbolsegment der ursprünglichen Elf-Datei nicht entfernt wird, kann das entsprechende Funktionssymbol gefunden werden (überprüfen Sie es mit readelf -S).

Nachdem der Rahmen ermittelt wurde, kann der Inhalt in jeder Ebene des Rahmens in Kombination mit der Baugruppe grundsätzlich zum Ableiten der Prozessvariablen verwendet werden.

Oben finden Sie eine kurze Analyse des detaillierten Inhalts des Funktionsaufrufprozesses unter der ARM-Architektur. Weitere Informationen zum Funktionsaufrufprozess unter der ARM-Architektur finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Beispielerklärung der Alarmfunktion in Linux
  • PHP führt 6 Linux-Befehlsfunktionscodebeispiele aus
  • Detaillierte Erklärung zur Verwendung der Stat-Funktion und des Stat-Befehls in Linux
  • So erhalten Sie die aktuelle Zeit mit der Funktion time(NULL) und localtime() in Linux
  • So fügen Sie einer Python-Funktion unter Linux/Mac ein Timeout hinzu
  • Linux-Unlink-Funktion und wie man Dateien löscht
  • Detaillierte Erklärung zur Verwendung der Linux-lseek-Funktion

<<:  Beispielcode mit SCSS in Uni-App

>>:  Detaillierte Erklärung zur Verwendung von Element-Plus in Vue3

Artikel empfehlen

Lösungen zur Verarbeitung und Reparatur historischer Linux-Images

Der ECS-Cloud-Server, der mit dem historischen Li...

Eine kurze Diskussion über die Rolle leerer HTML-Links

Leerer Link: Das heißt, es besteht keine Verbindu...

Detaillierte Erklärung gängiger Vorlagenbefehle in docker-compose.yml-Dateien

Hinweis: Beim Schreiben der Datei docker-compose....

Implementierungsmethode für den React State-Zustand und Lebenszyklus

1. Methoden zur Implementierung von Komponenten:組...

So erstellen Sie MySQL-Indizes richtig

Die Indizierung ähnelt dem Erstellen bibliografis...

Beispielcode zum Generieren eines QR-Codes mit js

Vor einiger Zeit musste das Projekt die Funktion ...

Implementierung von Debugging-Code über den Nginx-Reverse-Proxy

Hintergrund Heutzutage werden die Projekte des Un...

Verwenden Sie den Befehl sed, um die kv-Konfigurationsdatei in Linux zu ändern

sed ist ein Zeichenstromeditor unter Unix, also e...