web-dev-qa-db-fra.com

Comment aligner un pointeur en C

Est-il possible d'aligner un pointeur en C? Supposons que j'écrive des données dans une pile de tableau (le pointeur descend alors) et que je souhaite que les prochaines données que j'écrive soient alignées sur 4 afin que les données soient écrites dans un emplacement mémoire multiple de 4, comment procéder cette?

J'ai 

 uint8_t ary[1024];
 ary = ary+1024;
 ary -= /* ... */

Supposons maintenant que ary pointe sur l'emplacement 0x05. Je veux qu'il pointe vers 0x04. Maintenant, je pourrais juste faire 

ary -= (ary % 4);

mais C ne permet pas le modulo sur les pointeurs. Existe-t-il une solution indépendante de l'architecture?

23
Mark

Les tableaux ne sont PAS des pointeurs, malgré tout ce que vous avez pu lire dans les réponses erronées (ce qui concerne cette question en particulier ou Stack Overflow en général - ou ailleurs).

Vous ne pouvez pas modifier la valeur représentée par le nom d'un tableau, comme indiqué.

Ce qui est déroutant, peut-être, est que si ary est un paramètre de fonction, il semblera que vous puissiez ajuster le tableau:

void function(uint8_t ary[1024])
{
    ary += 213; // No problem because ary is a uint8_t pointer, not an array
    ...
}

Les tableaux en tant que paramètres de fonctions sont différents des tableaux définis en dehors d'une fonction ou à l'intérieur d'une fonction.

Tu peux faire:

uint8_t    ary[1024];
uint8_t   *stack = ary + 510;
uintptr_t  addr  = (uintptr_t)stack;

if (addr % 8 != 0)
    addr += 8 - addr % 8;
stack = (uint8_t *)addr;

Cela garantit que la valeur dans stack est alignée sur une limite de 8 octets, arrondie. Votre question demande d'arrondir à une limite de 4 octets. Le code devient ainsi:

if (addr % 4 != 0)
    addr -= addr % 4;
stack = (uint8_t *)addr;

Oui, vous pouvez aussi le faire avec des masques de bits. Non plus:

addr = (addr + (8 - 1)) & -8;  // Round up to 8-byte boundary

ou:

addr &= -4;                    // Round down to a 4-byte boundary

Cela ne fonctionne correctement que si le LHS est une puissance de deux - pas pour des valeurs arbitraires. Le code avec les opérations de module fonctionnera correctement pour tout module (positif).

Voir aussi: Comment allouer de la mémoire alignée en utilisant uniquement la bibliothèque standard.


Code de démonstration

Gnzlbgcommenté :

Le code pour une puissance de deux pauses si j'essaie d'aligner, par exemple. uintptr_t (2) jusqu'à une limite de 1 octet (les deux sont des puissances de 2: 2 ^ 1 et 2 ^ 0). Le résultat est 1 mais devrait être 2 car 2 est déjà aligné sur une limite de 1 octet.

Ce code montre que le code d'alignement est correct - à condition d'interpréter correctement les commentaires juste au-dessus (clarifié à présent par les mots "l'un ou l'autre" séparant les opérations de masquage de bits; je me suis fait prendre lors de la première vérification du code).

Les fonctions d'alignement pourraient être écrites de manière plus compacte, notamment sans les assertions, mais le compilateur optimisera la production du même code à partir de ce qui est écrit et de ce qui pourrait être écrit. Certaines affirmations pourraient également être rendues plus strictes. Et peut-être que la fonction de test devrait imprimer l’adresse de base de la pile avant de faire quoi que ce soit.

Le code pourrait, et devrait peut-être, vérifier qu’il n’y aura ni débordement ni dépassement numérique dans l’arithmétique. Ce serait plus probablement un problème si vous aligniez des adresses sur une limite de plusieurs mégaoctets. tant que vous gardez moins de 1 Ko, alignements, il est peu probable que vous trouviez un problème si vous n'essayez pas de sortir des limites des tableaux auxquels vous avez accès. (Strictement, même si vous effectuez des alignements de plusieurs mégaoctets, vous ne rencontrerez aucun problème si le résultat se situe dans la plage de mémoire allouée au tableau que vous manipulez.)

#include <assert.h>
#include <stdint.h>
#include <stdio.h>

/*
** Because the test code works with pointers to functions, the inline
** function qualifier is moot.  In 'real' code using the functions, the
** inline might be useful.
*/

/* Align upwards - arithmetic mode (hence _a) */
static inline uint8_t *align_upwards_a(uint8_t *stack, uintptr_t align)
{
    assert(align > 0 && (align & (align - 1)) == 0); /* Power of 2 */
    assert(stack != 0);

    uintptr_t addr  = (uintptr_t)stack;
    if (addr % align != 0)
        addr += align - addr % align;
    assert(addr >= (uintptr_t)stack);
    return (uint8_t *)addr;
}

/* Align upwards - bit mask mode (hence _b) */
static inline uint8_t *align_upwards_b(uint8_t *stack, uintptr_t align)
{
    assert(align > 0 && (align & (align - 1)) == 0); /* Power of 2 */
    assert(stack != 0);

    uintptr_t addr  = (uintptr_t)stack;
    addr = (addr + (align - 1)) & -align;   // Round up to align-byte boundary
    assert(addr >= (uintptr_t)stack);
    return (uint8_t *)addr;
}

/* Align downwards - arithmetic mode (hence _a) */
static inline uint8_t *align_downwards_a(uint8_t *stack, uintptr_t align)
{
    assert(align > 0 && (align & (align - 1)) == 0); /* Power of 2 */
    assert(stack != 0);

    uintptr_t addr  = (uintptr_t)stack;
    addr -= addr % align;
    assert(addr <= (uintptr_t)stack);
    return (uint8_t *)addr;
}

/* Align downwards - bit mask mode (hence _b) */
static inline uint8_t *align_downwards_b(uint8_t *stack, uintptr_t align)
{
    assert(align > 0 && (align & (align - 1)) == 0); /* Power of 2 */
    assert(stack != 0);

    uintptr_t addr  = (uintptr_t)stack;
    addr &= -align;                         // Round down to align-byte boundary
    assert(addr <= (uintptr_t)stack);
    return (uint8_t *)addr;
}

static inline int inc_mod(int x, int n)
{
    assert(x >= 0 && x < n);
    if (++x >= n)
        x = 0;
    return x;
}

typedef uint8_t *(*Aligner)(uint8_t *addr, uintptr_t align);

static void test_aligners(const char *tag, Aligner align_a, Aligner align_b)
{
    const int align[] = { 64, 32, 16, 8, 4, 2, 1 };
    enum { NUM_ALIGN = sizeof(align) / sizeof(align[0]) };
    uint8_t stack[1024];
    uint8_t *sp = stack + sizeof(stack);
    int dec = 1;
    int a_idx = 0;

    printf("%s\n", tag);
    while (sp > stack)
    {
        sp -= dec++;
        uint8_t *sp_a = (*align_a)(sp, align[a_idx]);
        uint8_t *sp_b = (*align_b)(sp, align[a_idx]);
        printf("old %p, adj %.2d, A %p, B %p\n",
               (void *)sp, align[a_idx], (void *)sp_a, (void *)sp_b);
        assert(sp_a == sp_b);
        sp = sp_a;
        a_idx = inc_mod(a_idx, NUM_ALIGN);
    }
    putchar('\n');
}

int main(void)
{
    test_aligners("Align upwards", align_upwards_a, align_upwards_b);
    test_aligners("Align downwards", align_downwards_a, align_downwards_b);
    return 0;
}

Exemple de sortie (partiellement tronqué):

Align upwards
old 0x7fff5ebcf4af, adj 64, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4be, adj 32, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4bd, adj 16, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4bc, adj 08, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4bb, adj 04, A 0x7fff5ebcf4bc, B 0x7fff5ebcf4bc
old 0x7fff5ebcf4b6, adj 02, A 0x7fff5ebcf4b6, B 0x7fff5ebcf4b6
old 0x7fff5ebcf4af, adj 01, A 0x7fff5ebcf4af, B 0x7fff5ebcf4af
old 0x7fff5ebcf4a7, adj 64, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4b7, adj 32, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4b6, adj 16, A 0x7fff5ebcf4c0, B 0x7fff5ebcf4c0
old 0x7fff5ebcf4b5, adj 08, A 0x7fff5ebcf4b8, B 0x7fff5ebcf4b8
old 0x7fff5ebcf4ac, adj 04, A 0x7fff5ebcf4ac, B 0x7fff5ebcf4ac
old 0x7fff5ebcf49f, adj 02, A 0x7fff5ebcf4a0, B 0x7fff5ebcf4a0
old 0x7fff5ebcf492, adj 01, A 0x7fff5ebcf492, B 0x7fff5ebcf492
…
old 0x7fff5ebcf0fb, adj 08, A 0x7fff5ebcf100, B 0x7fff5ebcf100
old 0x7fff5ebcf0ca, adj 04, A 0x7fff5ebcf0cc, B 0x7fff5ebcf0cc
old 0x7fff5ebcf095, adj 02, A 0x7fff5ebcf096, B 0x7fff5ebcf096

Align downwards
old 0x7fff5ebcf4af, adj 64, A 0x7fff5ebcf480, B 0x7fff5ebcf480
old 0x7fff5ebcf47e, adj 32, A 0x7fff5ebcf460, B 0x7fff5ebcf460
old 0x7fff5ebcf45d, adj 16, A 0x7fff5ebcf450, B 0x7fff5ebcf450
old 0x7fff5ebcf44c, adj 08, A 0x7fff5ebcf448, B 0x7fff5ebcf448
old 0x7fff5ebcf443, adj 04, A 0x7fff5ebcf440, B 0x7fff5ebcf440
old 0x7fff5ebcf43a, adj 02, A 0x7fff5ebcf43a, B 0x7fff5ebcf43a
old 0x7fff5ebcf433, adj 01, A 0x7fff5ebcf433, B 0x7fff5ebcf433
old 0x7fff5ebcf42b, adj 64, A 0x7fff5ebcf400, B 0x7fff5ebcf400
old 0x7fff5ebcf3f7, adj 32, A 0x7fff5ebcf3e0, B 0x7fff5ebcf3e0
old 0x7fff5ebcf3d6, adj 16, A 0x7fff5ebcf3d0, B 0x7fff5ebcf3d0
old 0x7fff5ebcf3c5, adj 08, A 0x7fff5ebcf3c0, B 0x7fff5ebcf3c0
old 0x7fff5ebcf3b4, adj 04, A 0x7fff5ebcf3b4, B 0x7fff5ebcf3b4
old 0x7fff5ebcf3a7, adj 02, A 0x7fff5ebcf3a6, B 0x7fff5ebcf3a6
old 0x7fff5ebcf398, adj 01, A 0x7fff5ebcf398, B 0x7fff5ebcf398
…
old 0x7fff5ebcf0f7, adj 01, A 0x7fff5ebcf0f7, B 0x7fff5ebcf0f7
old 0x7fff5ebcf0d3, adj 64, A 0x7fff5ebcf0c0, B 0x7fff5ebcf0c0
old 0x7fff5ebcf09b, adj 32, A 0x7fff5ebcf080, B 0x7fff5ebcf080
42
Jonathan Leffler

N'UTILISEZ PAS MODULO !!! IL IS est vraiment lent !!! Le moyen le plus rapide d'aligner un pointeur consiste à utiliser le calcul du complément à 2. Vous devez inverser les bits, en ajouter un et masquer les bits les moins significatifs 2 (pour 32 bits) ou 3 (pour 64 bits). Le résultat est un décalage que vous ajoutez ensuite à la valeur du pointeur pour l'aligner. Fonctionne très bien pour les nombres 32 et 64 bits. Pour un alignement sur 16 bits, masquez simplement le pointeur avec 0x1 et ajoutez cette valeur. L'algorithme fonctionne de la même manière dans tous les langages mais, comme vous pouvez le constater, le C++ embarqué est nettement supérieur au C dans tous les sens.

#include <cstdint>
/** Returns the number to add to align the given pointer to a 8, 16, 32, or 64-bit 
    boundary.
    @author Cale McCollough.
    @param  ptr The address to align.
    @return The offset to add to the ptr to align it. */
template<typename T>
inline uintptr_t MemoryAlignOffset (const void* ptr) {
    return ((~reinterpret_cast<uintptr_t> (ptr)) + 1) & (sizeof (T) - 1);
}

/** Word aligns the given byte pointer up in addresses.
    @author Cale McCollough.
    @param ptr Pointer to align.
    @return Next Word aligned up pointer. */
template<typename T>
inline T* MemoryAlign (T* ptr) {
    uintptr_t offset = MemoryAlignOffset<uintptr_t> (ptr);
    char* aligned_ptr = reinterpret_cast<char*> (ptr) + offset;
    return reinterpret_cast<T*> (aligned_ptr);
}

Pour une rédaction détaillée et des preuves, veuillez consulter @see https://github.com/kabuki-starship/kabuki-toolkit/wiki/Fastest-Method-to-Align-Pointers . Si vous souhaitez voir la preuve de pourquoi vous ne devriez jamais utiliser modulo, j'ai inventé l'algorithme le plus rapide au monde pour les entiers. Le repère sur le papier vous montre l’effet de l’optimisation d’une seule instruction modulo. S'il vous plaît @voir https://github.com/kabuki-starship/kabuki-toolkit/wiki/Engineering-a-Faster-Integer-to-String-Algorithm .

 Graph of why you shouldn't use modulo

1
user2356685

J'édite cette réponse parce que:

  1. J'ai eu un bug dans mon code original (j'ai oublié un transtypage de caractères à intptr_t), et
  2. Je réponds aux critiques de Jonathan Leffler afin de clarifier mon intention.

Le code ci-dessous n'est pas censé impliquer que vous pouvez changer la valeur d'un tableau (foo). Mais vous pouvez obtenir un pointeur aligné dans ce tableau, et cet exemple illustre une façon de le faire.

#define         alignmentBytes              ( 1 << 2 )   // == 4, but enforces the idea that that alignmentBytes should be a power of two
#define         alignmentBytesMinusOne      ( alignmentBytes - 1 )

uint8_t         foo[ 1024 + alignmentBytesMinusOne ];
uint8_t         *fooAligned;

fooAligned = (uint8_t *)((intptr_t)( foo + alignmentBytesMinusOne ) & ~alignmentBytesMinusOne);
1
par