Vor kurzem hatte ich eine eingeschränkte C-Programmieraufgabe im Tweet unter https://twitter.com/reiya_2200/status/1130761526959267841.
Das, was mir in den Sinn kommt, ist die Hauptrekursion, aber sie ist nicht sehr interessant, also habe ich versucht, sie in die Maschinensprache einzubetten, also habe ich sie als Material ausprobiert.
Voraussetzung ist x86_64 Linux + gcc und die Reproduktionsumgebung ist Ubuntu18 (WSL / Windows10). ** Selbst unter demselben Linux kann sich das Verhalten in verschiedenen Umgebungen erheblich ändern **. Bitte seien Sie vorsichtig, um nicht schlecht zu sein.
Tweet war also ** der folgende Code, in den die Maschinensprache gehorsam eingebettet war **.
emb1.c
#include <stdio.h>
int main(int argc,char *argv[]) {
static char __attribute__((section(".text"))) s[]="1\xc0H\x83\xc7\bH\x8b\27H\x85\xd2t\t\xf\xbe\22\215D\20\xd0\xeb\xeb\xc3";
return !printf("%d\n",((int(*)(void*))s)(argv));
}
Bei der Ausführung wird die Summe der im Argument angegebenen einstelligen Zahlen wie unten gezeigt ordnungsgemäß ausgegeben.
Kompilieren / ausführen
$ gcc emb1.c
/tmp/ccfyvQPW.s: Assembler messages:
/tmp/ccfyvQPW.s:3: Warning: ignoring changed section attributes for .text
$ ./a.out 4 6 4 9
23
Ich denke, es ist unnötig für diejenigen, die daran gewöhnt sind, aber ich werde es vorerst erklären.
Die Richtlinie besteht darin, den Teil zu implementieren, der die Summe als Funktion berechnet und in eine Maschinensprache übersetzt.
Für eine einfache Implementierung können Sie sich Code wie folgt vorstellen:
sum.c
int sum(void *pv) {
char **pc=pv;
int s=0;
while ( *++pc ) {
s+=**pc-'0';
}
return s;
}
Das Argument "pv" geht davon aus, dass "argv" übergeben wird. Das durch "argv" angegebene Array "char *" wird am Ende mit einem NULL-Zeiger abgeschlossen, sodass es tatsächlich ohne "argc" verarbeitet werden kann.
Wenn Sie dies kompilieren und die Maschinensprache überprüfen, sieht es folgendermaßen aus:
Kompilieren / rückwärts montieren
$ gcc -c -Os -fno-asynchronous-unwind-tables -fno-stack-protector sum.c && objdump -SCr sum.o
sum.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <sum>:
0: 31 c0 xor %eax,%eax
2: 48 83 c7 08 add $0x8,%rdi
6: 48 8b 17 mov (%rdi),%rdx
9: 48 85 d2 test %rdx,%rdx
c: 74 09 je 17 <sum+0x17>
e: 0f be 12 movsbl (%rdx),%edx
11: 8d 44 10 d0 lea -0x30(%rax,%rdx,1),%eax
15: eb eb jmp 2 <sum+0x2>
17: c3 retq
Dies bedeutet, dass die Maschinensprache des Funktionsteils eine Codesequenz von 31, c0,48,83, ... hexadezimal ist. Wenn dies in druckbaren Zeichen im ASCII-Zeichenbereich ausgedrückt wird, kann es als "1 \ xc0H \ x83 \ xc7 \ bH \ x8b \ 27H \ x85 \ xd2t \ t \ xf \ xbe \ 22 \ 215D \ 20 " ausgedrückt werden. Es wird xd0 \ xeb \ xeb \ xc3 "` sein.
Beachten Sie, dass gccs -Os
(Größenoptimierung) und andere Optionen, die verhindern, dass zusätzliche Verarbeitung in den Code eingebettet wird, angegeben werden, um den Code zu verkürzen. Ich wollte eine Option **, um es wenn möglich als ASCII-Zeichen zu kürzen, aber ich kann nichts dagegen tun, weil es nicht existiert.
Also habe ich die Maschinensprache des Funktionsteils verstanden.
Sie können dies als normale Zeichenfolge in Ihren Code einbetten.
Jetzt müssen Sie nur noch die Zeichenfolgenadresse als Funktionsadresse behandeln und damit die Funktion aufrufen. In printf
ist es ((int (*) (void *)) s) (argv)
undint (*) (void *)
ist der Adresstyp dieser Funktion. (Ein Zeiger auf eine Funktion, die "void *" als Argument verwendet und "int" zurückgibt) und in diese umwandelt.
emb1.c(Erneut veröffentlichen)
#include <stdio.h>
int main(int argc,char *argv[]) {
static char __attribute__((section(".text"))) s[]="1\xc0H\x83\xc7\bH\x8b\27H\x85\xd2t\t\xf\xbe\22\215D\20\xd0\xeb\xeb\xc3";
return !printf("%d\n",((int(*)(void*))s)(argv));
}
Es sind jedoch zwei Punkte zu beachten. Es geht um die Platzierung der Zeichenfolge s
.
Derzeit gibt es einen Mechanismus, der verhindert, dass unerwartete Speicherbereiche als Code ausgeführt werden. Ohne die oben genannten Spezifikationen verursacht SEGV, selbst wenn Sie versuchen, den entsprechenden Maschinensprachencode auszuführen. Dies ist erforderlich, wenn der Compiler nicht korrekt feststellen kann, ob es sich um ausführbaren Code handelt, wie in diesem Fall.
Ich bin jedoch ein wenig unzufrieden mit dem obigen Code. Dies liegt daran, dass das Attribut die ELF-Abschnittsstruktur sichtbar macht.
Gibt es eine intelligentere Möglichkeit, Maschinensprache einzubetten? Also habe ich den folgenden Code geschrieben.
emb2.c
#include <stdio.h>
int main(int argc,char *argv[]) {
asm volatile(".string \"H\\x83\\xc6\\bH\\x8b\\x6H\\x85\\xc0t\\xf\\xf\\xbe\\0\\x8d|\\x7\\xcf\\x89|$\\f\\xeb\\xe7\\xb0\"");
return !printf("%d\n",argc-1);
}
Sie können sehen, dass es immer noch funktioniert.
Kompilieren / ausführen
$ gcc emb2.c
$ ./a.out 4 6 4 9
23
Natürlich sind die Samen einfach. Wenn Sie den Pseudobefehl ".string" mit dem Befehl "asm volatile" angeben, der den Assembler-Code in den C-Sprachcode einbettet, wird ** die Maschinensprache, die der Zeichenfolge entspricht, an dieser Stelle eingebettet **.
Lassen Sie uns das nach dem Kompilieren erstellte "a.out" rückwärts zusammenbauen und überprüfen.
Rückwärts montieren
$ objdump -SCr a.out | sed -ne '/<main>:$/,+20p'
000000000000064a <main>:
64a: 55 push %rbp
64b: 48 89 e5 mov %rsp,%rbp
64e: 48 83 ec 10 sub $0x10,%rsp
652: 89 7d fc mov %edi,-0x4(%rbp)
655: 48 89 75 f0 mov %rsi,-0x10(%rbp)
659: 48 83 c6 08 add $0x8,%rsi
65d: 48 8b 06 mov (%rsi),%rax
660: 48 85 c0 test %rax,%rax
663: 74 0f je 674 <main+0x2a>
665: 0f be 00 movsbl (%rax),%eax
668: 8d 7c 07 cf lea -0x31(%rdi,%rax,1),%edi
66c: 89 7c 24 0c mov %edi,0xc(%rsp)
670: eb e7 jmp 659 <main+0xf>
672: b0 00 mov $0x0,%al
674: 8b 45 fc mov -0x4(%rbp),%eax
677: 83 e8 01 sub $0x1,%eax
67a: 89 c6 mov %eax,%esi
67c: 48 8d 3d a1 00 00 00 lea 0xa1(%rip),%rdi # 724 <_IO_stdin_used+0x4>
683: b8 00 00 00 00 mov $0x0,%eax
688: e8 93 fe ff ff callq 520 <printf@plt>
Dies ist der Teil, in den 48,83, c6,08 an Adresse 659 bis b0,00 an Adresse 672 eingebettet sind. Der Befehl an der Adresse 672 wird nicht tatsächlich verarbeitet, sondern als Befehl mit der Endung 00 vorbereitet, damit das NUL-Zeichen (00) zur eingebetteten Zeichenfolge hinzugefügt werden kann.
Nun zu diesem eingebetteten Code.
Dies setzt einfach die folgende Verarbeitung voraus.
Mit anderen Worten, speichern wir das Berechnungsergebnis so, wie es mit argc
als Summe ist.
Originalcode
#include <stdio.h>
int main(int argc,char *argv[]) {
while ( *++argv ) { argc+=**argv-'0'-1; }
return !printf("%d\n",argc-1);
}
Übrigens wird gemäß der Funktionsaufrufkonvention von x86_64 Linux das erste Argument "argc" im rdi-Register gespeichert (32-Bit-Teil ist edi) und das zweite Argument "argv" wird im rsi-Register gespeichert. Wenn Sie sie also direkt schleifen, ist dies in Ordnung.
Anwendbare Montage
659: add $0x8,%rsi #Erweitern Sie argv um ein Element
65d: mov (%rsi),%rax #Laden Sie argv-Elemente in rax
660: test %rax,%rax #NULL Zeiger Urteil
663: je 674 #Wechseln Sie unmittelbar nach dem eingebetteten Teil, wenn ein NULL-Zeiger erkannt wird
665: movsbl (%rax),%eax #Lesen Sie Zeichen in eax
668: lea -0x31(%rdi,%rax,1),%edi # argc(edi)Hinzufügen
66c: mov %edi,0xc(%rsp) #Speichern Sie edi im Stapelbereich
670: jmp 659 #Zum Anfang des eingebetteten Teils springen
672: mov $0x0,%al #Dummy-Anweisung
Ohne Optimierung schien das während printf verwendete argc
den Wert zu verwenden, der einmal im Stapel gespeichert war, nicht direkt im Register. Daher wird das Ergebnis der Änderung des esi-Registers durch den Befehl an der Adresse 66c im Stapel wiedergegeben.
Im Gegensatz dazu wird bei der Optimierung der Stapelbereich zum Speichern von "argc" nicht vorbereitet, so dass der Befehl an der Adresse 66c den Stapel zerstört. Die Ausgabe ist also wie beabsichtigt, aber beachten Sie, dass sie SEGV am Ausgang von "main" verursacht.
Geben Sie Optimierungsoptionen an
$ gcc -O3 emb2.c
$ ./a.out 4 6 4 9
23
Segmentation fault (core dumped)
Was haben Sie gedacht. Ich führte den Code ein und dachte, ich könnte das Gefühl fühlen, das Stufe -9 von [Korrupter C-Programmierer Stufe -10] entspricht (http://d.hatena.ne.jp/w_o/20060808#p4).
Schließlich werde ich vorerst auch die Hauptrekursive belassen, die die erwartete Lösung des Fragestellers zu sein scheint (weil sie vorerst zusammengestellt werden kann!).
Angenommene Lösung?Code
#include <stdio.h>
int main(int argc,char *argv[]) {
return argc>0 ? !printf("%d\n",main(0,argv+1)) : *argv ? **argv-'0'+main(0,argv+1) : 0;
}
Recommended Posts