有什么方法可以阻止来自 x86_64 上的 c++ 标准库的未对齐访问?

any way to stop unaligned access from c++ standard library on x86_64?

我正在尝试检查我的程序中是否有任何未对齐的读取。我通过(在 linux 内核 3.19 上的 g++ 上使用 x86_64)启用未对齐访问处理器异常:

asm volatile("pushf \n"
             "pop %%rax \n"
             "or [=11=]x40000, %%rax \n"
             "push %%rax \n"
             "popf \n" ::: "rax");

我做了一个可选的强制未对齐读取,它触发了异常,所以我知道它的工作原理。在我禁用它之后,我在一段代码中得到了一个错误,否则看起来很好:

char fullpath[eMaxPath];
    snprintf(fullpath, eMaxPath, "%s/%s", "blah", "blah2");

堆栈跟踪通过 __memcpy_sse2 显示失败,这让我怀疑标准库正在使用 sse 来实现我的 memcpy,但它没有意识到我现在已经使未对齐读取不可接受。

我的想法是否正确,有什么办法解决这个问题(即我可以让标准库使用未对齐的保险箱 sprintf/memcpy 代替)吗?

谢谢

你不会喜欢它,但只有一个答案:不要 link 反对标准库。通过更改该设置,您已经更改了 ABI,而标准库不喜欢它。 memcpy 和朋友们是手写的程序集,所以说服编译器做其他事情不是编译器选项的问题。

虽然我不想打消一个令人钦佩的想法,但我的朋友,你是在玩火。

它不仅是 sse2 访问,而且是 任何 未对齐的访问。即使是简单的 int 获取。


这是一个测试程序:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>

void *intptr;

void
require_aligned(void)
{
    asm volatile("pushf \n"
             "pop %%rax \n"
             "or [=10=]x00040000, %%eax \n"
             "push %%rax \n"
             "popf \n" ::: "rax");
}

void
relax_aligned(void)
{
    asm volatile("pushf \n"
             "pop %%rax \n"
             "andl [=10=]xFFFBFFFF, %%eax \n"
             "push %%rax \n"
             "popf \n" ::: "rax");
}

void
msg(const char *str)
{
    int len;

    len = strlen(str);
    write(1,str,len);
}

void
grab(void)
{
    volatile int x = *(int *) intptr;
}

int
main(void)
{

    setlinebuf(stdout);

    // minimum alignment from malloc is [usually] 8
    intptr = malloc(256);
    printf("intptr=%p\n",intptr);

    // normal access to aligned pointer
    msg("normal\n");
    grab();

    // enable alignment check exception
    require_aligned();

    // access aligned pointer under check [will be okay]
    msg("aligned_norm\n");
    grab();

    // this grab will generate a bus error
    intptr += 1;
    msg("aligned_except\n");
    grab();

    return 0;
}

这个输出是:

intptr=0x1996010
normal
aligned_norm
aligned_except
Bus error (core dumped)

程序生成这个只是因为试图从地址 0x1996011 获取 4 字节 int [这是 oddnot 4 的倍数。

所以,一旦你打开 AC [对齐检查] 标志,即使是简单的东西也会崩溃。


IMO,如果你确实有一些东西没有最佳对齐并且试图找到它们,使用printf,用调试断言检测你的代码,或者将 gdb 与一些特殊的 watch 命令或带有条件语句的断点一起使用是 better/safer 的方法


更新:

I a using my own custom allocator am preparing my code to run on an architecture that doesnt suport unaligned read/writes so I want to make sure my code will not break on that architecture.

很公平。

旁注: 我的好奇心战胜了我,因为我记得 [目前] 唯一有此问题的 [主要] 拱门是摩托罗拉 mc68000 和较旧的 IBM 大型机(例如 IBM System 370)。

我好奇的一个实际原因是对于某些架构(例如 ARM/android、MIPS)有可用的仿真器。如果需要,您可以从源代码重建模拟器,添加任何额外的检查。否则,在模拟器下进行调试可能是一种选择。

I can trap unaligned read/write using either the asm , or via gdb but both cause SIGBUS which i cant continue from in gdb and im getting too many false positives from std library (in the sense that their implementation would be aligned access only on the target).

我可以根据经验告诉您,在此之后尝试从信号处理程序恢复不太有效 [如果有的话]。如果您可以通过在标准函数中关闭 AC 来消除误报,那么使用 gdb 是最好的选择 [见下文]。

Ideally i guess i would like to use something like perf to show me callstacks that have misaligned but so far no dice.

这是可能的,但您必须验证 perf 是否报告了它们。要查看,您可以针对我上面的原始测试程序尝试 perf。如果有效,"counter" 之前应该是零,之后应该是一。


最干净的方法可能是在您的代码中添加 "assert" 宏 [可以使用 -DDEBUG 开关编译进出]。

但是,既然您已经费心打下了基础,那么看看 AC 方法是否可行可能是值得的。

由于您正在尝试调试您的 内存分配器,您只需要在您的 函数中打开AC。如果您的函数之一调用 libc,请禁用 AC,调用该函数,然后重新启用 AC。

内存分配器是相当底层的,所以它不能依赖太多的标准函数。大多数标准函数都依赖于能够调用 malloc。因此,您可能还想考虑一个与 [标准] 库的其余部分的 vtable 接口。

我编写了一些稍微不同的 AC 位 set/clear 函数。我将它们放入 .S 函数中以消除内联汇编的麻烦。

我已经在三个文件中编写了一个简单的示例用法。

这里是 AC set/clear 函数:

// acbit/acops.S -- low level AC [alignment check] operations

#define AC_ON       [=12=]x00040000
#define AC_OFF      [=12=]xFFFFFFFFFFFBFFFF

    .text

// acpush -- turn on AC and return previous mask
    .globl  acpush
acpush:
    // get old mask
    pushfq
    pop     %rax

    mov     %rax,%rcx                   // save to temp
    or      AC_ON,%ecx                  // turn on AC bit

    // set new mask
    push    %rcx
    popfq

    ret

// acpop -- restore previous mask
    .globl  acpop
acpop:
    // get current mask
    pushfq
    pop     %rax

    and     AC_OFF,%rax                 // clear current AC bit

    and     AC_ON,%edi                  // isolate the AC bit in argument
    or      %edi,%eax                   // lay it in

    // set new mask
    push    %rax
    popfq

    ret

// acon -- turn on AC
    .globl  acon
acon:
    jmp     acpush

// acoff -- turn off AC
    .globl  acoff
acoff:
    // get current mask
    pushfq
    pop     %rax

    and     AC_OFF,%rax                 // clear current AC bit

    // set new mask
    push    %rax
    popfq

    ret

这是一个包含函数原型和一些 "helper" 宏的头文件:

// acbit/acbit.h -- common control

#ifndef _acbit_acbit_h_
#define _acbit_acbit_h_

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>

typedef unsigned long flags_t;

#define VARIABLE_USED(_sym) \
    do { \
        if (1) \
            break; \
        if (!! _sym) \
            break; \
    } while (0)

#ifdef ACDEBUG
#define ACPUSH \
    do { \
        flags_t acflags = acpush()

#define ACPOP \
        acpop(acflags); \
    } while (0)

#define ACEXEC(_expr) \
    do { \
        acoff(); \
        _expr; \
        acon(); \
    } while (0)
#else
#define ACPUSH          /**/
#define ACPOP           /**/
#define ACEXEC(_expr)   _expr
#endif

void *intptr;

flags_t
acpush(void);

void
acpop(flags_t omsk);

void
acon(void);

void
acoff(void);

#endif

这是一个使用以上所有内容的示例程序:

// acbit/acbit2 -- sample allocator

#include <acbit.h>

// mymalloc1 -- allocation function [raw calls]
void *
mymalloc1(size_t len)
{
    flags_t omsk;
    void *vp;

    // function prolog
    // NOTE: do this on all "outer" (i.e. API) functions
    omsk = acpush();

    // do lots of stuff ...
    vp = NULL;

    // encapsulate standard library calls like this to prevent false positives:
    acoff();
    printf("%p\n",vp);
    acon();

    // function epilog
    acpop(omsk);

    return vp;
}

// mymalloc2 -- allocation function [using helper macros]
void *
mymalloc2(size_t len)
{
    void *vp;

    // function prolog
    ACPUSH;

    // do lots of stuff ...
    vp = NULL;

    // encapsulate standard library calls like this to prevent false positives:
    ACEXEC(printf("%p\n",vp));

    // function epilog
    ACPOP;

    return vp;
}

int
main(void)
{
    int x;

    setlinebuf(stdout);

    // minimum alignment from malloc is [usually] 8
    intptr = mymalloc1(256);
    intptr = mymalloc2(256);

    x = *(int *) intptr;

    return x;
}

更新#2:

I like the idea of disabling the check before any library calls.

如果 AC H/W 工作并且您包装库调用,这应该会产生 no 误报。唯一的例外是编译器调用 its 内部帮助程序库(例如,在 32 位机器上进行 64 位除法等)。

成为 ELF 加载程序(例如 /lib64/ld-linux-x86-64.so.2)的 aware/wary,对 "lazy" 符号绑定进行动态符号解析。应该不是什么大问题。如有必要,有一些方法可以强制重定位发生在程序启动时。

I have given up on perf for this as it seems to show me garbage even for a simple program like the one you wrote.

内核中的 perf 代码非常复杂,可能带来的麻烦得不偿失。它必须通过管道 [IIRC] 与 perf 程序通信。此外,执行 AC 的事情 [可能] 并不常见,以至于内核的代码路径没有经过很好的测试。

Im using ocperf with misalign_mem_ref.loads and stores but either way the counters dont correlate at all. If i record and look at the callstacks i get completely unrecognizable callstacks for these counters so i suspect either the counter doesnt work on my hardware/perf or it doesnt actually count what i think it counts

老实说,我不知道 perf 是否 [或不] 正确处理对不同内核的进程重新安排——它应该 [IMO]。但是,使用 sched_setaffinity 将程序锁定到单个内核可能会有所帮助。

但是,IMO,使用 AC 位更直接和明确。我认为这是更好的选择。


我已经谈到在代码中添加 "assert" 宏。

我在下面编写了一些代码。这些就是我要用的。它们独立于 AC 代码。但是,它们也可以在 "belt and suspenders" 方法中与 AC 位代码结合使用。

这些宏有一个明显的优势。当正确 [和自由] 插入时,它们可以在 计算 时检查错误的指针值。也就是说,更接近问题的真正根源。

使用 AC,您可能会计算出一个错误的值,但 AC 只会在 [某个时候] 之后,当指针被 取消引用 [这可能不会发生在您的 API 代码].

我之前已经完成了一个完整的内存分配器 [使用溢出检查和 "guard" 页等]。我使用的是宏观方法。而且,如果我只有一种工具,我会使用它。所以,我首先推荐它。

但是,正如我所说,它也可以与 AC 代码一起使用。

这是宏的头文件:

// acbit/acptr.h -- alignment check macros

#ifndef _acbit_acptr_h_
#define _acbit_acptr_h_

#include <stdio.h>

typedef unsigned int u32;

// bit mask for given width
#define ACMSKOFWID(_wid) \
    ((1u << (_wid)) - 1)

#ifdef ACDEBUG2
#define ACPTR_MSK(_ptr,_msk) \
    acptrchk(_ptr,_msk,__FILE__,__LINE__)
#else
#define ACPTR_MSK(_ptr,_msk)        /**/
#endif

#define ACPTR_WID(_ptr,_wid) \
    ACPTR_MSK(_ptr,(_wid) - 1)

#define ACPTR_TYPE(_ptr,_typ) \
    ACPTR_WID(_ptr,sizeof(_typ))

// acptrfault -- pointer alignment fault
void
acptrfault(const void *ptr,const char *file,int lno);

// acptrchk -- check pointer for given alignment
static inline void
acptrchk(const void *ptr,u32 msk,const char *file,int lno)
{
#ifdef ACDEBUG2

#if ACDEBUG2 >= 2
    printf("acptrchk: TRACE ptr=%p msk=%8.8X file='%s' lno=%d\n",
        ptr,msk,file,lno);
#endif

    if (((unsigned long) ptr) & msk)
        acptrfault(ptr,file,lno);
#endif
}

#endif

这是 "fault" 处理函数:

// acbit/acptr -- alignment check macros

#include <acbit/acptr.h>
#include <acbit/acbit.h>
#include <stdlib.h>

// acptrfault -- pointer alignment fault
void
acptrfault(const void *ptr,const char *file,int lno)
{

    // NOTE: it's easy to set a breakpoint on this function

    printf("acptrfault: pointer fault -- ptr=%p file='%s' lno=%d\n",
        ptr,file,lno);

    exit(1);
}

并且,这是一个使用它们的示例程序:

// acbit/acbit3 -- sample allocator using check macros

#include <acbit.h>
#include <acptr.h>

static double static_array[20];

// mymalloc3 -- allocation function
void *
mymalloc3(size_t len)
{
    void *vp;

    // get something valid
    vp = static_array;

    // do lots of stuff ...
    printf("BEF vp=%p\n",vp);

    // check pointer
    // NOTE: these can be peppered after every [significant] calculation
    ACPTR_TYPE(vp,double);

    // do something bad ...
    vp += 1;
    printf("AFT vp=%p\n",vp);

    // check again -- this should fault
    ACPTR_TYPE(vp,double);

    return vp;
}

int
main(void)
{
    int x;

    setlinebuf(stdout);

    // minimum alignment from malloc is [usually] 8
    intptr = mymalloc3(256);

    x = *(int *) intptr;

    return x;
}

程序输出如下:

BEF vp=0x601080
acptrchk: TRACE ptr=0x601080 msk=00000007 file='acbit/acbit3.c' lno=22
AFT vp=0x601081
acptrchk: TRACE ptr=0x601081 msk=00000007 file='acbit/acbit3.c' lno=29
acptrfault: pointer fault -- ptr=0x601081 file='acbit/acbit3.c' lno=29

我在这个例子中省略了 AC 代码。在您的真实目标系统上,intptr in main would/should 的取消引用在对齐时出错,但请注意执行时间线中的延迟时间。

就像我对这个问题的评论一样,asm 并不安全,因为它 。相反,使用

asm volatile ("add $-128, %rsp\n\t"
    "pushf\n\t"
    "orl [=10=]x40000, (%rsp)\n\t"
    "popf\n\t"
    "sub $-128, %rsp\n\t"
    );

-128 适合符号扩展的 8 位立即数,但 128 不适合,因此使用 add $-128 减去 128。)

或者在这种情况下,有专门的指令来切换该位,比如进位和方向标志:

asm("stac");   // Set AC flag
asm("clac");   // Clear AC flag

当您的代码使用未对齐的内存时,了解一下是个好主意。更改代码以避免在每种情况下都不一定是个好主意。有时将数据更紧密地打包在一起的更好的局部性更有价值。

鉴于您不一定要以消除所有未对齐访问为目标,我认为这不是找到您拥有的访问的最简单方法。

现代 x86 硬件具有对未对齐 loads/stores 的快速硬件支持。当它们不跨越高速缓存行边界或导致存储转发停顿时,实际上没有惩罚。

您可能会尝试查看其中一些事件的性能计数器:

  misalign_mem_ref.loads     [Speculative cache line split load uops dispatched to L1 cache]
  misalign_mem_ref.stores    [Speculative cache line split STA uops dispatched to L1 cache]

  ld_blocks.store_forward    [This event counts loads that followed a store to the same address, where the data could not be forwarded inside the pipeline from the store to the load.
                             The most common reason why store forwarding would be blocked is when a load's address range overlaps with a preceeding smaller uncompleted store.
                             See the table of not supported store forwards in the Intel? 64 and IA-32 Architectures Optimization Reference Manual.
                             The penalty for blocked store forwarding is that the load must wait for the store to complete before it can be issued.]

(来自我的 Sandybridge ocperf.py list output CPU)。

可能还有其他方法可以检测未对齐的内存访问。也许是 valgrind?我搜索了 valgrind detect unaligned and found this mailing list discussion from 13 years ago。可能还没有实现。


手动优化的库函数确实使用未对齐访问,因为这是它们完成工作的最快方式。例如将字符串的第 6 到 13 字节复制到其他地方可以而且应该只用一个 8 字节 load/store.

所以是的,您需要特殊的缓慢且安全的库函数版本。


如果您的代码必须执行额外的指令来避免使用未对齐的加载,那通常是不值得的。特别是如果输入是 通常 对齐的,在开始主循环之前有一个循环执行第一个对齐边界元素可能只会减慢速度。在对齐的情况下,一切都以最佳方式工作,没有检查对齐的开销。在未对齐的情况下,事情可能会慢几个百分点,但只要未对齐的情况很少见,就不值得避免它们。

Esp。如果它不是 SSE 代码,因为非 AVX 遗留 SSE 只能在保证对齐的情况下将负载折叠到 ALU 指令的内存操作数中。

为未对齐的内存操作提供足够好的硬件支持的好处是软件在对齐的情况下可以更快。它可以将对齐处理留给硬件,而不是 运行 额外的指令来处理可能对齐的指针。 (Linus Torvalds 在 http://realworldtech.com/ 论坛上有一些关于此的有趣帖子,但它们不可搜索,所以我找不到。