Récemment, j'ai eu une tâche de programmation C contrainte dans le tweet à https://twitter.com/reiya_2200/status/1130761526959267841.
Celui qui me vient à l'esprit est la récursivité principale, mais ce n'est pas très intéressant, alors j'ai essayé de l'intégrer en langage machine, donc je l'ai essayé en tant que matériau.
Comme prérequis, x86_64 Linux + gcc et l'environnement de reproduction est Ubuntu18 (WSL / Windows10). ** Même avec le même Linux, le comportement peut changer considérablement dans différents environnements **. Veuillez faire attention à ne pas être mauvais.
Donc tweet était ** le code suivant avec le langage machine obéissant incorporé **.
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));
}
Lorsqu'il est exécuté, le total des nombres à un chiffre spécifiés dans l'argument est affiché correctement comme indiqué ci-dessous.
Compiler / exécuter
$ 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
Je pense que ce n'est pas nécessaire pour ceux qui y sont habitués, mais je vais l'expliquer pour le moment.
La politique consiste à implémenter la partie qui calcule la somme en tant que fonction et à la traduire dans un langage machine.
Pour une implémentation facile, vous pouvez penser à un code comme celui-ci:
sum.c
int sum(void *pv) {
char **pc=pv;
int s=0;
while ( *++pc ) {
s+=**pc-'0';
}
return s;
}
L'argument pv
suppose que ʻargvest passé. Le tableau
char * indiqué par ʻargv
se termine par un pointeur NULL à la fin, il peut donc être traité sans ʻargc`.
Si vous compilez ceci et vérifiez le langage machine, cela ressemblera à ceci:
Compiler / assembler à l'envers
$ 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
Cela signifie que le langage machine de la partie fonction sera une séquence de code de 31, c0,48,83, ... en hexadécimal. Si cela est exprimé en caractères imprimables dans la plage de caractères ASCII, il peut être exprimé sous la forme `` 1 \ xc0H \ x83 \ xc7 \ bH \ x8b \ 27H \ x85 \ xd2t \ t \ xf \ xbe \ 22 \ 215D \ 20 . Ce sera xd0 \ xeb \ xeb \ xc3 "`.
Notez que -Os
(optimisation de la taille) de gcc et d'autres options qui empêchent un traitement supplémentaire d'être incorporé dans le code sont spécifiées pour raccourcir le code. Je voulais une option ** pour le raccourcir en tant que caractère ASCII si possible, mais je ne peux pas m'en empêcher car il n'existe pas.
J'ai donc compris le langage machine de la partie fonction.
Vous pouvez l'incorporer dans votre code sous forme de chaîne normale.
Tout ce que vous avez à faire maintenant est de traiter l'adresse de la chaîne comme l'adresse de la fonction et de l'utiliser pour appeler la fonction. Dans printf
, c'est ((int (*) (void *)) s) (argv)
, et ʻint (*) (void *) est le type d'adresse de cette fonction. (Un pointeur vers une fonction qui prend
void * comme argument et retourne ʻint
) et y effectue une conversion.
emb1.c(Republier)
#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));
}
Cependant, il y a deux points à noter. Il s'agit du placement de la chaîne s
.
static
pour éviter qu'il ne soit placé dans la zone de pile.text
) dans la section ELF.À ce stade, il existe un mécanisme pour empêcher les zones de mémoire inattendues d'être exécutées en tant que code. Sans les spécifications ci-dessus, même si vous essayez d'exécuter le code de langage machine correspondant, cela provoquera SEGV. C'est une connaissance nécessaire au cas où le compilateur ne peut pas le juger correctement comme un code exécutable, comme dans ce cas.
Cependant, je ne suis pas satisfait du code ci-dessus. C'est parce que l'attribut rend visible la structure de la section ELF.
Existe-t-il un moyen plus intelligent d'intégrer le langage machine? J'ai donc écrit le code suivant.
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);
}
Vous pouvez voir que cela fonctionne toujours.
Compiler / exécuter
$ gcc emb2.c
$ ./a.out 4 6 4 9
23
Bien sûr, les graines sont faciles. Si vous spécifiez la pseudo-instruction .string
avec l'instruction ʻasm volatile` qui intègre le code d'assemblage dans le code de langage C, ** le langage machine correspondant à la chaîne de caractères est incorporé à cet endroit **.
Vérifions le ʻa.out` qui a été créé après la compilation en le désassemblant.
Assembler inversé
$ 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>
C'est la partie où 48,83, c6,08 à l'adresse 659 à b0,00 à l'adresse 672 sont intégrés. L'instruction à l'adresse 672 n'est pas réellement traitée, mais elle est préparée comme une instruction se terminant par 00 afin que le caractère NUL (00) puisse être ajouté à la chaîne de caractères incorporée.
Maintenant, à propos de ce code intégré.
Cela suppose simplement le traitement suivant. En d'autres termes, sauvegardons le résultat du calcul tel quel avec ʻargc` comme total.
, nous essayons de la gérer en soustrayant une de plus au dernier
printf`.Code d'origine
#include <stdio.h>
int main(int argc,char *argv[]) {
while ( *++argv ) { argc+=**argv-'0'-1; }
return !printf("%d\n",argc-1);
}
En passant, selon la convention d'appel de fonction de x86_64 Linux, le premier argument ʻargc est stocké dans le registre rdi (la partie 32 bits est edi), et le second argument ʻargv
est stocké dans le registre rsi. Donc, si vous les bouclez directement, c'est OK.
Assemblage applicable
659: add $0x8,%rsi #Avance argv d'un élément
65d: mov (%rsi),%rax #Charger des éléments argv dans rax
660: test %rax,%rax #Jugement de pointeur NULL
663: je 674 #Passer immédiatement après la partie incorporée lorsqu'un pointeur NULL est détecté
665: movsbl (%rax),%eax #Lire des caractères dans eax
668: lea -0x31(%rdi,%rax,1),%edi # argc(edi)Ajouter à
66c: mov %edi,0xc(%rsp) #Enregistrer edi dans la zone de la pile
670: jmp 659 #Aller au début de la pièce incorporée
672: mov $0x0,%al #Instruction factice
Cependant, sans optimisation, ʻargc` utilisé au moment de printf semblait utiliser la valeur une fois sauvegardée dans la pile, pas directement dans le registre. Par conséquent, le résultat du changement du registre esi est reflété dans la pile par l'instruction à l'adresse 66c.
Au contraire, si l'optimisation est effectuée, la zone de pile pour sauvegarder l'argc n'est pas préparée, donc l'instruction à l'adresse 66c détruira la pile. Donc, la sortie est comme prévu, mais sachez que cela provoquera SEGV à la sortie de main
.
Spécifier les options d'optimisation
$ gcc -O3 emb2.c
$ ./a.out 4 6 4 9
23
Segmentation fault (core dumped)
Qu'as-tu pensé. J'ai introduit le code, pensant que je pouvais ressentir la sensation équivalente au niveau -9 du Niveau de programmeur C corrompu -10.
Enfin, pour le moment, je laisserai également le principal récursif, qui semble être la solution attendue du questionneur (car il peut être assemblé pour le moment!).
Solution supposée?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