Variables de registre explicites
https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)
Je pense que cela devrait maintenant être généralement l'approche recommandée par rapport aux contraintes de registre car :
- il peut représenter tous les registres, y compris
r8
,r9
etr10
qui sont utilisés pour les arguments d'appel système :comment spécifier des contraintes de registre sur le registre Intel x86_64 r8 à r15 dans l'assemblage en ligne GCC ? - c'est la seule option optimale pour les autres ISA en plus de x86 comme ARM, qui n'ont pas les noms de contrainte de registre magique :comment spécifier un registre individuel comme contrainte dans l'assemblage en ligne ARM GCC ? (en plus d'utiliser un registre temporaire + clobbers + et une instruction mov supplémentaire)
- Je soutiendrai que cette syntaxe est plus lisible que l'utilisation de mnémoniques à une seule lettre tels que
S -> rsi
Les variables de registre sont utilisées par exemple dans la glibc 2.29, voir :sysdeps/unix/sysv/linux/x86_64/sysdep.h
.
main_reg.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
register int64_t rax __asm__ ("rax") = 1;
register int rdi __asm__ ("rdi") = fd;
register const void *rsi __asm__ ("rsi") = buf;
register size_t rdx __asm__ ("rdx") = size;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi), "r" (rsi), "r" (rdx)
: "rcx", "r11", "memory"
);
return rax;
}
void my_exit(int exit_status) {
register int64_t rax __asm__ ("rax") = 60;
register int rdi __asm__ ("rdi") = exit_status;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub en amont.
Compiler et exécuter :
gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
-pedantic -o main_reg.out main_reg.c
./main.out
echo $?
Sortie
hello world
0
À titre de comparaison, l'analogue suivant à Comment invoquer un appel système via syscall ou sysenter dans l'assemblage en ligne ? produit un assemblage équivalent :
main_constraint.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (1), "D" (fd), "S" (buf), "d" (size)
: "rcx", "r11", "memory"
);
return ret;
}
void my_exit(int exit_status) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (60), "D" (exit_status)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub en amont.
Démontage des deux avec :
objdump -d main_reg.out
est presque identique, voici le main_reg.c
un :
Disassembly of section .text:
0000000000001000 <my_write>:
1000: b8 01 00 00 00 mov $0x1,%eax
1005: 0f 05 syscall
1007: c3 retq
1008: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
100f: 00
0000000000001010 <my_exit>:
1010: b8 3c 00 00 00 mov $0x3c,%eax
1015: 0f 05 syscall
1017: c3 retq
1018: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
101f: 00
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: bf 01 00 00 00 mov $0x1,%edi
102a: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: ba 0d 00 00 00 mov $0xd,%edx
1043: b8 01 00 00 00 mov $0x1,%eax
1048: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104f: 0a
1050: 0f 05 syscall
1052: 31 ff xor %edi,%edi
1054: 48 83 f8 0d cmp $0xd,%rax
1058: b8 3c 00 00 00 mov $0x3c,%eax
105d: 40 0f 95 c7 setne %dil
1061: 0f 05 syscall
1063: c3 retq
Nous voyons donc que GCC a intégré ces minuscules fonctions d'appel système comme on le souhaiterait.
my_write
et my_exit
sont les mêmes pour les deux, mais _start
en main_constraint.c
est légèrement différent :
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102a: ba 0d 00 00 00 mov $0xd,%edx
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: b8 01 00 00 00 mov $0x1,%eax
1043: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104a: 0a
104b: 89 c7 mov %eax,%edi
104d: 0f 05 syscall
104f: 31 ff xor %edi,%edi
1051: 48 83 f8 0d cmp $0xd,%rax
1055: b8 3c 00 00 00 mov $0x3c,%eax
105a: 40 0f 95 c7 setne %dil
105e: 0f 05 syscall
1060: c3 retq
Il est intéressant d'observer que dans ce cas GCC a trouvé un encodage équivalent légèrement plus court en choisissant :
104b: 89 c7 mov %eax,%edi
pour définir le fd
à 1
, ce qui équivaut à 1
à partir du numéro d'appel système, plutôt qu'un plus direct :
1025: bf 01 00 00 00 mov $0x1,%edi
Pour une discussion approfondie des conventions d'appel, voir aussi :Quelles sont les conventions d'appel pour les appels système UNIX et Linux (et les fonctions de l'espace utilisateur) sur i386 et x86-64
Testé dans Ubuntu 18.10, GCC 8.2.0.
Tout d'abord, vous ne pouvez pas utiliser en toute sécurité GNU C Basic asm("");
syntaxe pour cela (sans contraintes d'entrée/sortie/clobber). Vous avez besoin d'Extended asm pour informer le compilateur des registres que vous modifiez. Voir l'asm en ligne dans le manuel GNU C et le wiki de balises d'assemblage en ligne pour des liens vers d'autres guides pour des détails sur des choses comme "D"(1)
signifie dans le cadre d'un asm()
déclaration.
Vous avez également besoin de asm volatile
car ce n'est pas implicite pour Extended asm
instructions avec 1 ou plusieurs opérandes de sortie.
Je vais vous montrer comment exécuter des appels système en écrivant un programme qui écrit Hello World!
à la sortie standard en utilisant le write()
appel système. Voici la source du programme sans implémentation de l'appel système réel :
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size);
int main(void)
{
const char hello[] = "Hello world!\n";
my_write(1, hello, sizeof(hello));
return 0;
}
Vous pouvez voir que j'ai nommé ma fonction d'appel système personnalisée en tant que my_write
afin d'éviter les conflits de noms avec le write
"normal" , fourni par libc. Le reste de cette réponse contient la source de my_write
pour i386 et amd64.
i386
Les appels système dans i386 Linux sont implémentés à l'aide du 128e vecteur d'interruption, par ex. en appelant le int 0x80
dans votre code assembleur, après avoir défini les paramètres en conséquence au préalable, bien sûr. Il est possible de faire de même via SYSENTER
, mais l'exécution réelle de cette instruction est réalisée par le VDSO mappé virtuellement à chaque processus en cours d'exécution. Depuis SYSENTER
n'a jamais été conçu comme un remplacement direct du int 0x80
API, il n'est jamais directement exécuté par les applications utilisateur - à la place, lorsqu'une application a besoin d'accéder à du code du noyau, elle appelle la routine virtuellement mappée dans le VDSO (c'est ce que le call *%gs:0x10
dans votre code est pour), qui contient tout le code supportant le SYSENTER
instruction. Il y en a beaucoup à cause du fonctionnement réel de l'instruction.
Si vous voulez en savoir plus à ce sujet, consultez ce lien. Il contient un aperçu assez succinct des techniques appliquées dans le noyau et le VDSO. Voir aussi The Definitive Guide to (x86) Linux System Calls - certains appels système comme getpid
et clock_gettime
sont si simples que le noyau peut exporter du code + des données qui s'exécutent dans l'espace utilisateur afin que le VDSO n'ait jamais besoin d'entrer dans le noyau, ce qui le rend beaucoup plus rapide même que sysenter
pourrait être.
Il est beaucoup plus facile d'utiliser le int $0x80
plus lent pour invoquer l'ABI 32 bits.
// i386 Linux
#include <asm/unistd.h> // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"int $0x80"
: "=a" (ret)
: "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
: "memory" // the kernel dereferences pointer args
);
return ret;
}
Comme vous pouvez le voir, en utilisant le int 0x80
L'API est relativement simple. Le numéro du syscall va au eax
registre, tandis que tous les paramètres nécessaires au syscall vont respectivement dans ebx
, ecx
, edx
, esi
, edi
, et ebp
. Les numéros d'appel système peuvent être obtenus en lisant le fichier /usr/include/asm/unistd_32.h
.
Des prototypes et des descriptions des fonctions sont disponibles dans la 2ème section du manuel, donc dans ce cas write(2)
.
Le noyau enregistre/restaure tous les registres (sauf EAX) afin que nous puissions les utiliser comme opérandes d'entrée uniquement pour l'asm en ligne. Voir Quelles sont les conventions d'appel pour les appels système UNIX et Linux (et les fonctions de l'espace utilisateur) sur i386 et x86-64
Gardez à l'esprit que la liste de clobber contient également le memory
paramètre, ce qui signifie que l'instruction répertoriée dans la liste d'instructions fait référence à la mémoire (via le buf
paramètre). (Une entrée de pointeur vers asm inline n'implique pas que la mémoire pointée est également une entrée. Voir Comment puis-je indiquer que la mémoire *pointée* par un argument ASM inline peut être utilisée ?)
amd64
Les choses semblent différentes sur l'architecture AMD64 qui arbore une nouvelle instruction appelée SYSCALL
. Il est très différent de l'original SYSENTER
instruction, et certainement beaucoup plus facile à utiliser à partir d'applications utilisateur - cela ressemble vraiment à un CALL
normal , en fait, et en adaptant l'ancien int 0x80
au nouveau SYSCALL
est assez banal. (Sauf qu'il utilise RCX et R11 au lieu de la pile du noyau pour enregistrer le RIP et le RFLAGS de l'espace utilisateur afin que le noyau sache où retourner).
Dans ce cas, le numéro de l'appel système est encore passé dans le registre rax
, mais les registres utilisés pour contenir les arguments correspondent maintenant presque à la convention d'appel de fonction :rdi
, rsi
, rdx
, r10
, r8
et r9
dans cet ordre. (syscall
détruit lui-même rcx
donc r10
est utilisé à la place de rcx
, laissant les fonctions wrapper libc utiliser simplement mov r10, rcx
/ syscall
.)
// x86-64 Linux
#include <asm/unistd.h> // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"syscall"
: "=a" (ret)
// EDI RSI RDX
: "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
: "rcx", "r11", "memory"
);
return ret;
}
(Voir la compilation sur Godbolt)
Remarquez à quel point la seule chose à changer était pratiquement les noms de registre et l'instruction réelle utilisée pour effectuer l'appel. Ceci est principalement dû aux listes d'entrée/sortie fournies par la syntaxe d'assemblage en ligne étendue de gcc, qui fournit automatiquement les instructions de déplacement appropriées nécessaires à l'exécution de la liste d'instructions.
Le "0"(callnum)
la contrainte de correspondance pourrait être écrite sous la forme "a"
car l'opérande 0 (le "=a"(ret)
output) n'a qu'un seul registre parmi lequel choisir ; nous savons qu'il choisira EAX. Utilisez ce que vous trouvez le plus clair.
Notez que les systèmes d'exploitation non Linux, comme MacOS, utilisent des numéros d'appel différents. Et même différentes conventions de passage d'arguments pour 32 bits.