我正在尝试检查程序中的任何未对齐读取。我启用了未对齐的访问处理器异常(在Linux内核3.19上使用g ++上的x86_64):
asm volatile("pushf \n"
"pop %%rax \n"
"or $0x40000, %%rax \n"
"push %%rax \n"
"popf \n" ::: "rax");
我做一个可选的强制未对齐读取,触发异常,所以我知道它的工作原理。在我禁用之后,我在一段代码中出现错误,否则看起来很好:
char fullpath[eMaxPath];
snprintf(fullpath, eMaxPath, "%s/%s", "blah", "blah2");
stacktrace通过__memcpy_sse2
显示失败,这导致我怀疑标准库正在使用sse来实现我的memcpy但它没有意识到我现在已经使得未对齐的读取不可接受。
我的想法是否正确,是否有任何解决方法(即我可以使标准库使用未对齐的安全sprintf / memcpy)?
感谢
答案 0 :(得分:3)
虽然我讨厌劝阻一个令人钦佩的观念,但我的朋友却在玩火。
它不仅仅是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 $0x00040000, %%eax \n"
"push %%rax \n"
"popf \n" ::: "rax");
}
void
relax_aligned(void)
{
asm volatile("pushf \n"
"pop %%rax \n"
"andl $0xFFFBFFFF, %%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)
程序生成这个只是因为尝试从地址int
[奇数 < 4]。
因此,一旦打开AC [对齐检查]标志,即使是简单的事情也会破坏。
IMO,如果您确实有一些未正确对齐并且正在尝试使用0x1996011
找到它们,使用调试断言检测代码或使用{{1}使用一些特殊的printf
命令或带有条件语句的断点是更好/更安全的方式
<强>更新强>
我使用自己的自定义分配器准备我的代码在一个不支持未对齐读/写的架构上运行所以我想确保我的代码不会破坏该架构。
足够公平。
旁注:我的好奇心已经变得更好了我作为唯一的[主要]拱门我现在可以回忆起这个问题是摩托罗拉gdb
和更老的IBM大型机(例如IBM watch
)。
我好奇心的一个实际原因是,对于某些拱门(例如ARM / android,MIPS),可以使用仿真器。您可以从源重建模拟器,如果需要,可以添加任何额外的检查。否则,可以选择在模拟器下进行调试。
我可以使用asm或gdb来捕获未对齐的读/写但是两者都会导致我无法继续从gdb中获取的SIGBUS以及从std库中获取太多误报(从某种意义上说它们的实现将是对齐访问只在目标上。)
我可以从经验中告诉你,在此之后尝试从信号处理程序恢复并不能正常工作[如果有的话]。如果您可以通过在标准函数中使用AC off 来消除误报,那么使用mc68000
是最好的选择。[见下文]。
理想情况下,我想我想使用像perf这样的东西来向我展示那些未对齐但到目前为止没有骰子的书架。
这是可能的,但您必须验证System 370
是否会报告它们。要查看,您可以针对我上面的原始测试计划尝试gdb
。如果它有效,&#34;计数器&#34;应该在之前和之后为零。
最干净的方法可能是使用&#34;断言&#34;宏[可以使用perf
开关编译进出。
然而,由于您已经陷入了奠定基础的麻烦,因此可能值得看看AC方法是否可行。
由于您正在尝试调试您的内存分配器,因此您只需在 功能中启用AC。如果您的某个函数调用perf
,请禁用AC,调用该函数,然后重新启用AC。
内存分配器的级别相当低,因此它不能依赖太多的标准功能。大多数标准函数依赖于能够调用malloc。因此,您可能还需要考虑与[标准]库的其余部分的vtable接口。
我编写了一些稍微不同的AC位设置/清除功能。我将它们放入-DDEBUG
函数中以消除内联麻烦。
我在三个文件中编写了一个简单的样本用法。
以下是AC设置/清除功能:
libc
这是一个头文件,其中包含函数原型和一些&#34; helper&#34;宏:
.S
以下是使用以上所有内容的示例程序:
// acbit/acops.S -- low level AC [alignment check] operations
#define AC_ON $0x00040000
#define AC_OFF $0xFFFFFFFFFFFBFFFF
.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
更新#2:
我喜欢在任何库调用之前禁用检查的想法。
如果AC H / W工作并且您包装了库调用,则应该产生 no 误报。唯一的例外是编译器调用其内部帮助程序库(例如在32位机器上进行64位除法等)。
注意/警惕ELF加载程序(例如// 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
)在&#34; lazy&#34;上进行动态符号解析。符号的绑定。不应该是一个大问题。如有必要,有很多方法可以在程序启动时强制重定位。
我已经放弃了这个,因为它似乎向我显示垃圾,即使是像你写的那个简单的程序。
内核中的执行代码非常复杂,可能比它的价值更麻烦。它必须通过管道[IIRC]与perf程序通信。此外,执行AC事情[可能]并不常见,因此内核的代码路径未经过充分测试。
我使用ocperf与misalign_mem_ref.loads并存储,但无论哪种方式,计数器根本不相关。如果我记录并查看callstacks,我会得到这些计数器的完全无法识别的调用堆栈,所以我怀疑计数器在我的硬件/性能上是不起作用的,或者它实际上并不算我认为它的重要性
老实说,我不知道perf是否正确处理重新安排到不同核心[或不] - 它应该[IMO]。但是,使用// 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;
}
将程序锁定到单个核心可能有所帮助。
但是,使用AC位更直接,更明确,IMO。我认为这是更好的选择。
我已经谈过添加&#34;断言&#34;代码中的宏。
我在下面编了一些代码。这些是我使用的。它们独立于AC代码。但是,它们也可以与&#34;腰带和吊杆&#34; 方法中的AC位代码结合使用。
这些宏有一个明显的优势。正确[和自由]插入时,他们可以在计算计算时检查错误的指针值。也就是说,更接近问题的真正来源。
使用AC,您可能会计算出一个错误的值,但AC只会在[某个时间]稍后启动,此时指针取消引用 [这可能根本不会出现在您的API代码中]。
我在[超支检查和&#34;后卫&#34;之前)完成了一个完整的内存分配器。页面等]。我使用的是宏观方法。而且,如果我只有一个工具,那就是我使用的工具。所以,我首先推荐它。
但是,正如我所说,它也可以与AC代码一起使用。
这里是宏的头文件:
/lib64/ld-linux-x86-64.so.2
这是&#34;错误&#34;处理函数:
sched_setaffinity
而且,这是一个使用它们的示例程序:
// 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
这是程序输出:
// 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);
}
我在这个例子中没有使用AC代码。在您的真实目标系统上,// 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
的取消引用会在对齐上出现错误,但请注意执行时间线中的多少时间。
答案 1 :(得分:2)
你不会喜欢它,但只有一个答案:不要链接标准库。通过更改该设置,您已更改ABI,标准库不喜欢它。 memcpy和朋友都是手写的汇编,所以说服编译器做其他事情并不是编译器选项的问题。
答案 2 :(得分:2)
就像我对这个问题发表评论一样,asm并不安全,因为steps on the red-zone。相反,使用
asm volatile ("add $-128, %rsp\n\t"
"pushf\n\t"
"orl $0x40000, (%rsp)\n\t"
"popf\n\t"
"sub $-128, %rsp\n\t"
);
(-128
适合符号扩展的8位立即数,但128
不适用,因此使用add $-128
减去128。)
或者在这种情况下,有用于切换该位的专用指令,就像进位和方向标志一样:
asm("stac"); // Set AC flag
asm("stac"); // Clear AC flag
当你的代码使用未对齐的内存时,有一个想法是个好主意。改变代码以避免在任何情况下都不一定是个好主意。有时,将数据打包在一起更好的地方更有价值。
鉴于您不一定要以消除所有未对齐的访问为目标,我认为这不是找到您所拥有的访问的最简单方法。
现代x86硬件对未对齐的加载/存储具有快速硬件支持。当它们没有跨越缓存行边界或导致存储转发停顿时,实际上没有任何惩罚。
您可能会尝试查看其中某些事件的效果计数器:
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 CPU上的ocperf.py list
output)。
可能还有其他方法可以检测未对齐的内存访问。也许valgrind?我在valgrind detect unaligned上搜索了this mailing list discussion from 13 years ago。可能还没有实施。
手动优化的库函数确实使用了未对齐的访问,因为这是他们完成工作的最快方式。例如将字符串的字节6到13复制到其他地方可以而且应该只用一个8字节的加载/存储来完成。
所以是的,你需要特殊的慢速和安全版本的库函数。
如果您的代码必须执行额外的指令以避免使用未对齐的加载,那么它通常是不值得的。 ESP。如果输入通常对齐,在启动主循环之前有一个循环来执行第一个向上对齐边界元素可能只会减慢速度。在对齐的情况下,一切都是最佳的,没有检查对齐的开销。在未对齐的情况下,事情可能会慢一些,但只要未对齐的案例很少,就不值得避免它们。
ESP。如果它不是SSE代码,因为非AVX传统SSE只能在保证对齐时将负载折叠到ALU指令的存储器操作数中。
对未对齐的内存操作具有足够好的硬件支持的好处是,在对齐的情况下,软件可以更快。它可以将对齐处理留给硬件,而不是运行额外的指令来处理可能对齐的指针。 (Linus Torvalds在http://realworldtech.com/论坛上发布了一些有趣的帖子,但它们无法搜索到,所以我找不到它。