web-dev-qa-db-fra.com

Lecture d'une valeur de registre dans une variable C

Je me souviens avoir vu un moyen d'utiliser l'assembly inline gcc étendu pour lire une valeur de registre et la stocker dans une variable C.

Je ne peux cependant pas pour la vie de moi me rappeler comment former la déclaration asm.

40
Brian

Note de l'éditeur: cette façon d'utiliser une variable locale d'enregistrement-asm est désormais documentée par GCC comme "non prise en charge". Il arrive toujours de travailler sur GCC, mais rompt avec le bruit. (Je pense que cette formulation dans la documentation a été ajoutée après la publication de cette réponse.)

La version variable globale à registre fixe a un coût de performance élevé pour le x86 32 bits, qui ne dispose que de 7 registres d'entiers GP (sans compter le pointeur de pile). Cela réduirait cela à 6. N'envisagez cela que si vous avez une variable globale que tout votre code utilise fortement.


Aller dans une direction différente de celle des autres réponses jusqu'à présent, car je ne suis pas sûr de ce que vous voulez.

GCC Manual § 5.40 Variables in Specified Registers

register int *foo asm ("a5");

Ici a5 est le nom du registre à utiliser…

Naturellement, le nom du registre dépend du processeur, mais ce n'est pas un problème, car des registres spécifiques sont le plus souvent utiles avec des instructions d'assembleur explicites (voir Extended Asm ). Ces deux choses nécessitent généralement que vous conditionniez votre programme en fonction du type de processeur.

La définition d'une telle variable de registre ne réserve pas le registre; il reste disponible pour d'autres utilisations dans des endroits où le contrôle de flux détermine que la valeur de la variable n'est pas active.

GCC Manual § 3.18 Options for Code Generation Conventions

-ffixed- reg

Traitez le registre nommé reg comme un registre fixe; le code généré ne doit jamais s'y référer (sauf peut-être comme pointeur de pile, pointeur de trame ou dans un autre rôle fixe).

Cela peut reproduire la réponse de Richard d'une manière plus simple,

int main() {
    register int i asm("ebx");
    return i + 1;
}

même si cela n'a pas de sens, car vous n'avez aucune idée de ce qui se trouve dans le registre ebx.

Si vous avez combiné ces deux, compiler ceci avec gcc -ffixed-ebx,

#include <stdio.h>
register int counter asm("ebx");
void check(int n) {
    if (!(n % 2 && n % 3 && n % 5)) counter++;
}
int main() {
    int i;
    counter = 0;
    for (i = 1; i <= 100; i++) check(i);
    printf("%d Hamming numbers between 1 and 100\n", counter);
    return 0;
}

vous pouvez vous assurer qu'une variable C utilise toujours réside dans un registre pour un accès rapide et aussi ne sera pas encombrée par un autre code généré. (Handily, ebx est sauvegardé en tant qu'appelé sous les conventions d'appel x86 habituelles, donc même s'il est encombré par des appels à d'autres fonctions compilées sans -ffixed-*, il devrait également être restauré.)

D'un autre côté, ce n'est certainement pas portable et ce n'est généralement pas un avantage en termes de performances, car vous restreignez la liberté du compilateur.

31
ephemient

Voici un moyen d'obtenir ebx:

int main()
{
    int i;
    asm("\t movl %%ebx,%0" : "=r"(i));
    return i + 1;
}

Le résultat:

main:
    subl    $4, %esp
    #APP
             movl %ebx,%eax
    #NO_APP
    incl    %eax
    addl    $4, %esp
    ret

Le "= r" (i) est une contrainte de sortie, indiquant au compilateur que la première sortie (% 0) est un registre qui doit être placé dans la variable "i". À ce niveau d'optimisation (-O5), la variable i n'est jamais stockée en mémoire, mais est conservée dans le registre eax, qui se trouve également être le registre de valeur de retour.

20
Richard Pennington

Je ne connais pas gcc, mais en VS, voici comment:

int data = 0;   
__asm
{
    mov ebx, 30
    mov data, ebx
}
cout<<data;

Essentiellement, j'ai déplacé les données dans ebx vers votre variable data.

5
Jacob

Cela déplacera le registre du pointeur de pile dans la variable sp.

intptr_t sp;
asm ("movl %%esp, %0" : "=r" (sp) );

Remplacez simplement 'esp' par le registre réel qui vous intéresse (mais assurez-vous de ne pas perdre le %%) et 'sp' avec votre variable.

3
R Samuel Klatchko

Vous ne pouvez pas savoir quelle valeur le code généré par le compilateur aura stocké dans n'importe quel registre lors de l'exécution de votre instruction inline asm, donc la valeur n'a généralement aucun sens, et vous feriez bien mieux d'utiliser un débogueur pour regarder enregistrer des valeurs à l'arrêt à un point d'arrêt.

Cela étant dit, si vous allez faire cette étrange tâche, vous pourriez aussi bien le faire efficacement.

Sur certaines cibles (comme x86), vous pouvez utiliser des contraintes de sortie de registre spécifique pour indiquer au compilateur lequel enregistrer une sortie. Utiliser un registre spécifique contrainte de sortie avec un modèle asm vide (instructions zéro) pour indiquer au compilateur que votre instruction asm ne se soucie pas de cette valeur de registre en entrée, mais ensuite la variable C donnée sera dans ce registre .

#include <stdint.h>

int foo() {
    uint64_t rax_value;           // type width determines register size
    asm("" : "=a"(rax_value));  // =letter determines which register (or partial reg)

    uint32_t ebx_value;
    asm("" : "=b"(ebx_value));

    uint16_t si_value;
    asm("" : "=S"(si_value) );

    uint8_t sil_value;  // x86-64 required to use the low 8 of a reg other than a-d
       // With -m32:  error: unsupported size for integer register
    asm("# Hi mom, my output constraint picked %0" : "=S"(sil_value) );

    return sil_value + ebx_value;
}

Compilé avec clang5.0 sur Godbolt pour x86-64 . Notez que les 2 valeurs de sortie inutilisées sont optimisées, aucune paire de commentaires asm générée par le compilateur #APP/#NO_APP (Qui fait passer l'assembleur en mode d'analyse rapide, ou du moins n'est utilisé pour si ce n'est plus une chose). C'est parce que je n'ai pas utilisé asm volatile, Et ils ont un opérande de sortie donc ils ne sont pas implicitement volatile.

foo():                                # @foo()
# BB#0:
    Push    rbx
    #APP
    #NO_APP
    #DEBUG_VALUE: foo:ebx_value <- %EBX
    #APP
    # Hi mom, my output constraint picked %sil
    #NO_APP
    #DEBUG_VALUE: foo:sil_value <- %SIL
    movzx   eax, sil
    add     eax, ebx
    pop     rbx
    ret
                                    # -- End function
                                    # DW_AT_GNU_pubnames
                                    # DW_AT_external

Notez le code généré par le compilateur pour ajouter deux sorties ensemble, directement à partir des registres spécifiés. Notez également le Push/pop de RBX, car RBX est un registre préservé dans la convention d'appel x86-64 System V. (Et essentiellement toutes les conventions d'appel x86 32 et 64 bits). Mais nous avons dit au compilateur que notre instruction asm y écrit une valeur. (L'utilisation d'une instruction asm vide est une sorte de piratage; il n'y a pas de syntaxe pour dire directement au compilateur que nous voulons simplement lire un registre, car comme je l'ai dit, vous ne savez pas ce que le compilateur faisait avec les registres lorsque votre instruction asm est inséré.)

Le compilateur traitera votre instruction asm comme si elle - a écrit ce registre, donc s'il a besoin de la valeur pour plus tard, il l'aura copiée dans un autre registre (ou déversée en mémoire) lorsque votre asm l'instruction "s'exécute".


Les autres contraintes de registre x86 sont b (bl/bx/ebx/rbx), c (.../rcx ), d (.../rdx), S (sil/si/esi/rsi), D (.../rdi) . Il n'y a pas de contrainte spécifique pour bpl/bp/ebp/rbp, même si ce n'est pas spécial dans les fonctions sans pointeur de trame. (Peut-être parce que son utilisation rendrait votre code non compilateur avec -fno-omit-frame-pointer.)

Vous pouvez utiliser register uint64_t rbp_var asm ("rbp"), auquel cas asm("" : "=r" (rbp_var)); garantit que la contrainte "=r" Choisira rbp. De même pour r8-r15, qui n'ont pas non plus de contraintes explicites. Sur certaines architectures, comme ARM, les variables de registre asm sont le seul moyen de spécifier le registre que vous souhaitez pour les contraintes d'entrée/sortie asm. (Et notez que les contraintes asm sont les seulement utilisation supportée des variables register asm; il n'y a aucune garantie que la valeur de la variable sera dans ce registre à tout autre moment.


Rien n'empêche le compilateur de placer ces instructions asm où il veut dans une fonction (ou les fonctions parentes après l'inlining) . Vous n'avez donc aucun contrôle sur vous échantillonnez la valeur d'un registre. asm volatile Peut éviter une réorganisation, mais peut-être uniquement par rapport aux autres accès volatile. Vous pouvez vérifier l'asm généré par le compilateur pour voir si vous avez obtenu ce que vous vouliez, mais sachez que cela pourrait avoir été par hasard et pourrait se casser plus tard.

Vous pouvez placer une instruction asm dans la chaîne de dépendance pour que quelque chose d'autre contrôle où le compilateur la place . Utilisez une contrainte "+rm" Pour indiquer au compilateur qu'il modifie une autre variable qui est réellement utilisée pour quelque chose qui ne s'optimise pas.

uint32_t ebx_value;
asm("" : "=b"(ebx_value), "+rm"(some_used_variable) );

some_used_variable peut être une valeur de retour d'une fonction et (après un certain traitement) passé en argument à une autre fonction. Ou calculé dans une boucle, et sera retourné comme valeur de retour de la fonction. Dans ce cas, l'instruction asm est garantie de se produire à un moment donné après la fin de la boucle et avant tout code qui dépend de la dernière valeur de cette variable.

Cela va cependant vaincre les optimisations telles que la propagation constante pour cette variable. https://gcc.gnu.org/wiki/DontUseInlineAsm . Le compilateur ne peut pas supposer n'importe quoi sur la valeur de sortie; il ne vérifie pas que l'instruction asm n'a aucune instruction.


Cela ne fonctionne pas pour certains registres que gcc ne vous permettra pas d'utiliser comme opérandes de sortie ou clobbers, par ex. le pointeur de pile.

La lecture de la valeur dans une variable C peut avoir du sens pour un pointeur de pile, cependant, si votre programme fait quelque chose de spécial avec les piles.

Comme alternative à inline-asm, il y a __builtin_frame_address(0) pour obtenir une adresse de pile. (Mais IIRC, fait que cette fonction crée un cadre de pile complet, même lorsque -fomit-frame-pointer Est activé, comme c'est le cas par défaut sur x86.)

Pourtant, dans de nombreuses fonctions, c'est presque gratuit (et faire un cadre de pile peut être bon pour la taille du code, en raison des modes d'adressage plus petits pour RBP-relatif que RSP-relatif aux variables locales).

L'utilisation d'une instruction mov dans une instruction asm fonctionnerait bien sûr aussi.

2
Peter Cordes

Depuis les documents du GCC lui-même: http://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html

2
user108127
#include <stdio.h>

void gav(){
        //rgv_t argv = get();
        register unsigned long long i asm("rax");
        register unsigned long long ii asm("rbx");
        printf("I`m gav - first arguman is: %s - 2th arguman is: %s\n", (char *)i, (char *)ii);
}

int main(void)
{
    char *test = "I`m main";
    char *test1 = "I`m main2";
    printf("0x%llx\n", (unsigned long long)&gav);
    asm("call %P0" : :"i"((unsigned long long)&gav), "a"(test), "b"(test1));
    return 0;
}
1
Mahdi Mohammadi