如何在C语言中对齐指针

29

有没有办法在C语言中对齐指针?假设我正在向一个数组栈写入数据(因此指针向下移动),并且我希望我写入的下一个数据是4字节对齐的,这样数据就会被写入到4的倍数的内存地址中。我该怎么做?

我已经...

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

现在假设ary指向地址0x05,我想让它指向0x04。 现在我可以直接进行操作:

ary -= (ary % 4);

但是C语言不允许在指针上进行取模运算。有没有什么解决方案是与体系结构无关的?


@templatetypedef:我很想看到C++标准中说long可以保存指针的参考文献。我相信你的看法是错误的,但我愿意被证明是错的。 - Jonathan Leffler
@Jonathan Leffler- 看起来你是对的,指针不需要适合 long 类型!我一直在这个假设下操作了很长时间...我想知道我为什么最初会这样想? - templatetypedef
1
@templatetypedef:因为在大多数系统上,您可以做出这种假设,尽管标准并不保证它。ILP32和LP64(如果您仍然可以找到ILP64系统-DEC Alpha属于该类别)都可以正常工作。唯一普遍存在的不符合条件的系统是Windows 64-一个LLP64系统。 - Jonathan Leffler
2
@JonathanLeffler 在 C89 中(暗示)是必需的。微软在基本上所有其他人反对的情况下强行更改了 C99,使其不再是必需的,然后没有实现 C99。是的,我仍然很生气。 - zwol
6个回答

57

数组并不是指针,无论你在这个问题特定的答案中(或者 Stack Overflow 等其他地方)读到了什么误导性的回答。

你不能像所示那样改变数组名称表示的值。

可能令人困惑的是,如果ary是函数参数,它似乎可以调整数组:

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

函数参数中的数组与在函数外或函数内定义的数组不同。

您可以执行以下操作:

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;

这样可以确保stack中的值对齐在8字节边界上,向上舍入。你的问题要求向下舍入到4字节边界,因此代码需要更改为:

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

是的,你也可以使用位掩码来实现。有两种方法:

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

或者:

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

只有左操作数为2的幂时,此方法才能正确运行 —— 对于任意值都不适用。使用模运算的代码将对任何(正的)模数都能正确运行。

另请参阅:如何仅使用标准库分配对齐内存


演示代码

Gnzlbg 评论说道:

如果我尝试将例如uintptr_t(2)对齐到1字节边界(两者都是2的幂:2^1和2^0),那么2的幂的代码会出错。结果为1,但应该是2,因为2已经对齐到1字节边界。

这段代码证明了对齐代码是正确的,只要你正确理解上面的注释(现在通过“either or”单词分隔位掩码操作进行了澄清;当我第一次检查代码时,我被卡住了)。

对齐函数可以更紧凑地编写,特别是没有断言的情况下,但是编译器将优化以从所编写的内容和可能编写的内容中产生相同的代码。某些断言也可以更加严格。也许测试函数在做任何其他事情之前应该打印堆栈的基地址。

代码可以检查算术运算是否存在数字溢出或下溢问题,这可能更有可能成为问题,如果您将地址对齐到多兆字节边界;当您保持小于1 KiB的对齐方式时,如果您不尝试越界访问您可以访问的数组,则不太可能出现问题。 (严格来说,即使您进行多兆字节对齐,如果结果将在分配给操作的数组的内存范围内,则不会遇到麻烦。)

#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;
}

示例输出(部分截断):

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

这段代码在你想要对齐到2的幂以外的东西时会崩溃,但我不知道你是否会想要这样做:D - tom
1
@tom:是的,这段代码假设您想对齐到2的幂(因此如果您需要其他内容,它会出错)。我从未听说过需要其他内容的系统(例如,当所有事情都结束时,6字节对齐变成等效于2字节对齐)。 - Jonathan Leffler
@JonathanLeffler 在函数参数中,int arr[] 应该改为 int * const arr 而不是仅仅的 int *arr,对吗? - Ajay Brahmakshatriya
@AjayBrahmakshatriya:在函数中将int arr[]转换为const int *arrint *const arr没有特定的原因。如果您知道const的限定符是什么(提示:在我给出的两个选择中,它们不是同一件事),则可以这样做。这取决于您想要什么。通常情况下,在函数中使指针成为常量没有意义(无论是否更改都不会影响调用代码),如果数组被视为非常量,则没有理由使指针所指向的数据成为常量。 - Jonathan Leffler
如果你想做,你可以做 — 但是没有必要。 - Jonathan Leffler
显示剩余5条评论

4

不要使用模运算!它非常慢!最快的指针对齐方法是使用二进制补码数学。您需要反转位,加一,并屏蔽掉2(32位)或3(64位)个最低有效位。结果是一个偏移量,然后将其添加到指针值以进行对齐。适用于32位和64位数字。对于16位对齐,只需使用0x1屏蔽指针并添加该值即可。该算法在任何语言中都具有相同的功能,但如您所见,嵌入式C ++在各个方面都远优于C。

#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);
}

要查看详细的说明和证明,请参阅https://github.com/kabuki-starship/kabuki-toolkit/wiki/Fastest-Method-to-Align-Pointers。如果您想了解为什么永远不应该使用取模运算符,我发明了世界上最快的整数转字符串算法。本文的基准测试展示了优化一个取模指令的效果。请参阅https://github.com/kabuki-starship/kabuki-toolkit/wiki/Engineering-a-Faster-Integer-to-String-Algorithm。要查看为什么不应该使用取模的图表,请点击Graph of why you shouldn't use modulo

13
如果操作数是无符号整数,且模数是2的幂次方,则编译器会将取模运算优化成位运算。链接 https://gcc.godbolt.org/z/6tVTfN 为例。 - kirbyfan64sos
如果操作数不是无符号的,会怎么样? - undefined
@user16217248 如果使用有符号数进行指针操作,那么它可能不会被优化,但无论如何,这几乎肯定是一个糟糕的主意。 - undefined

2

我正在编辑这个答案,因为:

  1. 我原来的代码中有一个错误(我忘了将类型转换为 intptr_t),以及
  2. 我正在回复 Jonathan Leffler 的批评,以澄清我的意图。

下面的代码并不意味着你可以更改数组(foo)的值。但是你 可以 得到一个指向该数组的对齐指针,这个例子演示了一种方法。

#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);

2

由于某些原因,我无法使用模数或按位操作。在这种情况下:

void *alignAddress = (void*)((((intptr_t)address + align - 1) / align) * align) ;

对于C ++:

template <int align, typename T>
constexpr T padding(T value)
{
    return ((value + align - 1) / align) * align;
}
...
char* alignAddress = reinterpret_cast<char*>(padding<8>(reinterpret_cast<uintptr_t>(address)))

1

基于其他地方学到的技巧和阅读 @par 的答案,显然对于我这种32位机器的特殊情况,我所需要的是 ((size - 1) | 3) + 1,它的作用如下,我认为可能对其他人也有用。

for (size_t size = 0; size < 20; ++size) printf("%d\n", ((size - 1) | 3) + 1);

0
4
4
4
4
8
8
8
8
12
12
12
12
16
16
16
16
20
20
20

1

我正在使用它来对齐C语言中的指针:

#include <inttypes.h>
static inline void * please_align(void * ptr){
    char * res __attribute__((aligned(128))) ;
    res = (char *)ptr + (128 - (uintptr_t) ptr) % 128;
    return res ;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接