如何在C中对齐指针

Posted

技术标签:

【中文标题】如何在C中对齐指针【英文标题】:How to align a pointer in C 【发布时间】:2011-06-17 22:41:39 【问题描述】:

有没有办法在 C 中对齐指针?假设我正在将数据写入数组堆栈(因此指针向下)并且我希望我写入的下一个数据是 4 对齐的,因此数据被写入一个 4 的倍数的内存位置,我该怎么做那个?

我有

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

现在假设ary 指向位置0x05。我希望它指向0x04。 现在我可以做

ary -= (ary % 4);

但是 C 不允许对指针取模。是否有任何独立于架构的解决方案?

【问题讨论】:

@templatetypedef:我很想看看 C++ 标准中的引用,它说long 可以保存一个指针。我相信你的信念是错误的,但我愿意被证明是错误的。 @Jonathan Leffler- 看起来你是对的,指针不需要长!我一直在这个假设下工作了很长时间......我想知道我为什么首先想到这个? @templatetypedef:因为在大多数系统上,您可以摆脱这种假设,即使标准不保证这一点。 ILP32 和 LP64(以及 ILP64 系统,如果您仍然可以找到一个 - DEC Alpha 属于该类别)都可以正常工作。唯一不适用的流行系统是 Windows 64 - LLP64 系统。 @JonathanLeffler 它 C89 要求的(暗示)。微软几乎不顾其他所有人的反对,强行改变 C99 使其不再需要,然后没有实施 C99。是的,我还是很苦。 【参考方案1】:

数组是 NOT 指针,尽管您可能在此处的错误答案中读到了任何内容(特别是指这个问题或一般的堆栈溢出 - 或其他任何地方)。

您不能更改数组名称所代表的值,如图所示。

或许令人困惑的是,如果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

这只有在 LHS 是 2 的幂时才能正常工作——不适用于任意值。具有模运算的代码对于任何(正)模都可以正常工作。

另请参阅:How to allocate aligned memory using only the standard library


演示代码

Gnzlbgcommented:

如果我尝试对齐,则两次中断的代码,例如uintptr_t(2) 最多 1 字节边界(都是 2 的幂:2^1 和 2^0)。结果是 1,但应该是 2,因为 2 已经与 1 字节边界对齐。

此代码表明对齐代码是可以的——只要您正确解释上面的 cmets(现在通过分隔位掩码操作的“或”字进行澄清;我在第一次检查代码时被抓住了)。

对齐函数可以写得更紧凑,尤其是在没有断言的情况下,但是编译器会优化以根据所写和可以写的内容生成相同的代码。一些断言也可以变得更加严格。也许测试函数应该在做任何其他事情之前打印出堆栈的基地址。

代码可以并且也许应该检查算术是否存在数字上溢或下溢。如果您将地址对齐到数兆字节的边界,这将更有可能成为问题;当您保持低于 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:是的,这段代码假设你想要对齐到 2 的幂(所以如果你需要别的东西,它确实会中断)。我从来没有听说过需要其他任何东西的系统(例如,6 字节对齐就等于 2 字节对齐)。 @JonathanLeffler 如果我尝试对齐,则为两次中断的幂的代码,例如uintptr_t(2) 最多 1 字节边界(都是 2 的幂:2^1 和 2^0)。结果是 1 但应该是 2,因为 2 已经与 1 字节边界对齐。 @gnzlbg:对齐 1 字节边界(或 0 字节边界,如果可以说这样的事情有任何意义,我认为它不可以)没有意义)。在具有字节地址的现代机器上(与有时具有字地址并且需要额外的诡计来处理字节的旧机器相反),没有地址尚未在 1 字节边界上对齐,因此计算一个没有意义。但是,无论是否需要,显示的代码适用于 1 .. 64 的 2 次幂(参见演示代码),并且对于较大的对齐应该没问题,不会溢出(未检查)。 也许我遇到了与@JonathanLeffler 相同的“非此即彼”问题。如果有人发现它有用,我最终会做的是 auto align_up(Integer x, size_t a) return x + (a - 1) &amp; ~(a - 1); auto align_down(Integer x, size_t a) return self &amp; ~(alignment - 1); ,它们适用于非 2 x 和 2 a 的幂。【参考方案2】:

不要使用模数!!!真的很慢!!!毫无疑问,对齐指针的最快方法是使用 2 的补码数学。您需要反转位,加一,并屏蔽掉 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);

有关详细的写作和证明,请@see https://github.com/kabuki-starship/kabuki-toolkit/wiki/Fastest-Method-to-Align-Pointers。如果你想知道为什么你不应该使用模数,我发明了世界上最快的整数到字符串算法。纸上的基准向您展示了仅优化一条模指令的效果。请@see https://github.com/kabuki-starship/kabuki-toolkit/wiki/Engineering-a-Faster-Integer-to-String-Algorithm.

【讨论】:

如果操作数是无符号整数且模数是 2 的幂,编译器会将模运算优化为按位运算:gcc.godbolt.org/z/6tVTfN【参考方案3】:

由于某种原因,我不能使用模运算或按位运算。在这种情况下:

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

【讨论】:

【参考方案4】:

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

    我的原始代码中有一个错误(我忘记了对intptr_t 的类型转换),并且 我正在回复 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);

【讨论】:

【参考方案5】:

基于在其他地方学到的技巧,以及从阅读@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

【讨论】:

以上是关于如何在C中对齐指针的主要内容,如果未能解决你的问题,请参考以下文章

如何告诉 GCC 指针参数始终是双字对齐的?

存储 std::assume_aligned 指针 C++ 20

如何在 Objective C 中对齐内存?

C语言中如何对齐输出

如何检查内存地址在 C 中是不是为 32 位对齐

如何在 C 中分配 32 字节对齐的内存