4 votes

Transfert d'un pointeur vers un type non lié

J'essaie actuellement de me familiariser avec les règles strictes de l'aliasing en C et ce code, d'après ce que je comprends actuellement, les viole.

Nous avons converti un pointeur de tampon en pointeur de structure d'installation et, selon la norme C, cela devrait conduire à un comportement non défini, n'est-ce pas ?

static inline void libusb_fill_control_transfer(
    struct libusb_transfer *transfer, libusb_device_handle *dev_handle,
    unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data,
    unsigned int timeout)
{
    struct libusb_control_setup *setup = (struct libusb_control_setup *)(void *) buffer;
    transfer->dev_handle = dev_handle;
    transfer->endpoint = 0;
    transfer->type = LIBUSB_TRANSFER_TYPE_CONTROL;
    transfer->timeout = timeout;
    transfer->buffer = buffer;
    if (setup)
        transfer->length = (int) (LIBUSB_CONTROL_SETUP_SIZE
            + libusb_le16_to_cpu(setup->wLength));
    transfer->user_data = user_data;
    transfer->callback = callback;
}

Editar.

Cela fait partie du projet libusb https://github.com/libusb/libusb/blob/7ffad5c137ed4c1d8a3ac485f35770fb979ca53a/libusb/libusb.h#L1578

Edit 2.

Ajout de libusb_control_setup définition de la structure

struct libusb_control_setup {
    /** Request type. Bits 0:4 determine recipient, see
     * \ref libusb_request_recipient. Bits 5:6 determine type, see
     * \ref libusb_request_type. Bit 7 determines data transfer direction, see
     * \ref libusb_endpoint_direction.
     */
    uint8_t  bmRequestType;

    /** Request. If the type bits of bmRequestType are equal to
     * \ref libusb_request_type::LIBUSB_REQUEST_TYPE_STANDARD
     * "LIBUSB_REQUEST_TYPE_STANDARD" then this field refers to
     * \ref libusb_standard_request. For other cases, use of this field is
     * application-specific. */
    uint8_t  bRequest;

    /** Value. Varies according to request */
    uint16_t wValue;

    /** Index. Varies according to request, typically used to pass an index
     * or offset */
    uint16_t wIndex;

    /** Number of bytes to transfer */
    uint16_t wLength;
};

2voto

dbush Points 8590

En supposant que le paramètre buffer ne pointe pas réellement vers un objet de type struct libusb_control_setup (probablement un unsigned char ), alors oui, il s'agit d'une violation stricte de l'aliasing qui est comportement indéfini .

Les règles régissant l'aliasing sont spécifiées dans la section 6.5p7 de la norme C standard :

La valeur stockée d'un objet ne doit être accessible que par une lvalue. qui présente l'un des types suivants : 88)

  • un type compatible avec le type effectif de l'objet,
  • une version qualifiée d'un type compatible avec le type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant au type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, de manière récursive, un membre d'un sous-agrégat ou union contenue), ou
  • un type de caractère.

88 ) L'intention de cette liste est de spécifier les circonstances dans lesquelles dans lesquelles un objet peut ou ne peut pas être aliasé.

Notez que cela n'inclut pas le traitement d'un char comme s'il s'agissait d'un autre type, bien que l'inverse soit autorisé.

La bonne façon de gérer cette situation est de créer une structure locale du type donné, puis d'utiliser la fonction memcpy pour copier les octets.

struct libusb_control_setup setup;
memcpy(&setup, buffer, sizeof setup);

1voto

P__J__ Points 12922

En complément de la réponse de @dbush

En ce qui concerne l'exemple fourni, c'est ainsi que je l'aurais fait, mais mais comme il s'agit du code d'un projet respectable qui est manifestement utilisé dans beaucoup d'endroits, je ne suis pas sûr de ce qu'il faut en penser.

Le pointer punning est utilisé par de nombreux programmeurs, qui pensent qu'il est sûr parce qu'il fonctionne sur leurs ordinateurs. De nombreux programmeurs pensent également qu'utiliser memcpy rendra leur code moins efficace et plus gourmand en mémoire.

Dans la plupart des cas (quand c'est possible bien sûr), le compilateur optimisera l'utilisation de la fonction memcpy appelez

exemple :

typedef struct
{
    int a;
    double b;
    int (*callback)(int);
}mt;

int foo(char *ptr, int par)
{
    mt m;

    memcpy(&m, ptr, sizeof(m));

    printf("%f\n", m.b);
    m.callback(par);
    return m.a;
}

Le compilateur x86 produit du code :

.LC0:
        .string "%f\n"
foo:
        push    rbp
        mov     ebp, esi
        sub     rsp, 32
        movdqu  xmm1, XMMWORD PTR [rdi]
        mov     rax, QWORD PTR [rdi+16]
        mov     edi, OFFSET FLAT:.LC0
        movaps  XMMWORD PTR [rsp], xmm1
        movsd   xmm0, QWORD PTR [rsp+8]
        mov     QWORD PTR [rsp+16], rax
        mov     eax, 1
        call    printf
        mov     edi, ebp
        call    [QWORD PTR [rsp+16]]
        mov     eax, DWORD PTR [rsp]
        add     rsp, 32
        pop     rbp
        ret

Mais l'ARM Cortex M0 appellera la fonction memcpy car une version non alignée du pointeur provoquera une exception matérielle.

.LC0:
        .ascii  "%f\012\000"
foo:
        push    {r4, lr}
        movs    r4, r1
        sub     sp, sp, #32
        movs    r1, r0
        movs    r2, #24
        add     r0, sp, #8
        bl      memcpy
        ldr     r2, [sp, #16]
        ldr     r3, [sp, #20]
        ldr     r0, .L3
        str     r2, [sp]
        str     r3, [sp, #4]
        bl      printf
        movs    r0, r4
        ldr     r3, [sp, #24]
        blx     r3
        ldr     r0, [sp, #8]
        add     sp, sp, #32
        pop     {r4, pc}
.L3:
        .word   .LC0

0voto

supercat Points 25534

L'utilisation d'une structure ou d'un type union pour lire ou écrire des données à partir d'un tampon de caractères fourni en externe pose deux problèmes potentiels :

  1. La norme permet aux implémentations dont les utilisateurs n'auraient jamais besoin d'accéder au stockage sous différents types et à différents moments une large licence pour supposer que les programmes n'effectueront pas de tels accès. Bien qu'elle reconnaisse explicitement l'existence d'implémentations qui garantissent que tous les accès aux objets seront traités comme des accès au stockage sous-jacent en utilisant la sémantique précise de l'environnement d'exécution hôte (voir N1570 5.1.2.3 paragraphe 9), que la norme les y oblige ou non, et qu'elle permette aux programmes conformes (mais pas strictement) de cibler exclusivement ces implémentations, elle ne fait aucune distinction entre les implémentations qui offrent de telles garanties et celles qui ne le font pas.

  2. Le comportement du moulage d'un pointeur vers un type struct ou union n'est défini que si le pointeur satisfait aux exigences d'alignement les plus strictes de chacun de ses membres. Si la structure ou l'union a un membre dont l'exigence d'alignement n'est pas satisfaite par le pointeur, le casting vers la structure ou le type d'union invoquera un comportement non défini, et sur certaines implémentations réelles, il est susceptible de produire un code erroné. même si le pointeur satisfait à l'exigence d'alignement pour tous les membres qui sont effectivement utilisés. .

Le premier problème peut être résolu en utilisant une configuration de compilateur adaptée à la tâche à accomplir. Le second, en revanche, doit être gardé à l'esprit même si l'on utilise la configuration du compilateur -fno-strict-aliasing dialect . Vu quelque chose comme :

#include <string.h>
struct foo { short x, y; int z; } sf;
void test1(void *p)
{
    memcpy(&sf, p, sizeof (struct foo));
}
void test2(void *p)
{
    struct foo *foop = p;
    memcpy(&sf, foop, sizeof (struct foo));
}

En ciblant le Cortex-M0, clang générera du code pour test1 ce qui sera beaucoup moins efficace (une pénalité de vitesse et d'espace de près de 4:1) que test2 dans le cas où p est aligné sur les mots, mais le code pour test2 échouera si p n'est pas aligné sur un mot alors que le code plus lent de test1 fonctionnera quoi qu'il en soit.

Ainsi, même en utilisant memcpy Dans le cas d'un pointeur vers un type de structure, la décision d'effectuer un cast vers un type de structure doit prendre en compte ce que l'on sait de l'alignement. Un tel transfert peut améliorer considérablement l'efficacité si l'alignement est connu, mais faire planter le programme s'il ne l'est pas.

0voto

n.m. Points 30344

Libusb est écrit en C.

Pour faire face aux règles d'aliasing strictes de C, il suffit d'écrire votre application dans un langage différent. Assembly, Haskell, Java, Rust, et même C++ (oui !) sont tous bons. Les règles d'aliasing du C strict ne s'appliquent pas à ces langages.

Donc, du point de vue de la libusb, la buffer Le paramètre apparaît comme par magie de nulle part, sans qu'aucun objet C n'y vive.

La norme C impose-t-elle des exigences dans cette situation ? Bien sûr que non. La norme permet aux programmes C d'interagir avec du code écrit dans d'autres langages, mais aucune règle spécifique n'est prévue. Il s'agit donc d'un comportement indéfini au regard de la norme C. Permettez-moi de répéter : toute interaction de C avec d'autres langages est UB . Cependant, pour une raison quelconque, même les puristes du langage C les plus obstinés se satisfont de ce type spécifique de comportement indéfini, et nous pouvons donc supposer que cela est acceptable.

Mais que faire si vous aimez vraiment C ?

Vous pourriez écrire votre code en C, puis utiliser un compilateur C++ (n'oubliez pas qu'il n'y a pas de règles strictes d'aliasing pour les interactions entre C et C++). Mais c'est trop risqué. Les langages ont divergé et les mêmes constructions y ont parfois des significations différentes. Ce n'est donc pas une approche évolutive.

Il n'y avait pas de solution - jusqu'à maintenant. Mais j'ai inventé une solution, et par la présente, je mets mon invention dans le domaine public.

La voici : écrivez votre application client dans un nouveau langage appelé C ? Ce langage n'est pas très différent du C. Sa norme peut être obtenue en prenant la norme C (n'importe quelle version que vous voulez) et en remplaçant toutes les occurrences pertinentes de l'identifiant C con C?? . (Attention, ne remplacez pas toutes les occurrences, ou vous rendrez certains exemples de code invalides). Il y a plusieurs compilateurs libres et commerciaux pour ce langage facilement disponibles (gcc, clang, et msvc sont tous des compilateurs C ?? plus ou moins compétents).

Le langage C ?? a ses propres règles strictes en matière d'aliasing, mais bien sûr, elles ne s'appliquent qu'aux programmes C ?? Lorsqu'un objet est créé en C ?? et que son adresse est transmise au code C (ou vice versa), il n'existe aucune règle qui dicte la similarité ou la dissemblance des types C et C ?? concernés. Ils ne sont jamais "le même type" ou "des types compatibles" ou quelque chose comme ça, parce qu'il n'y a pas de règles qui régissent la compatibilité des types C et C ? Ainsi, on peut être, disons, libusb_control_setup et l'autre, disons, char[128] et nous ne sommes pas mieux ou moins bien lotis par rapport à la situation où les deux types sont appelés libusb_control_setup .

Alors oui, nous avons échangé un type d'UB contre un autre. Celui qui met les gens en colère, pour un autre contre lequel personne n'a jamais rien dit. Je dirais que c'est une bonne affaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X