gpt4 book ai didi

c - 为什么编译器生成 4 字节负载而不是 1 字节负载,而更宽的负载可能会访问未映射的数据?

转载 作者:太空狗 更新时间:2023-10-29 16:23:31 27 4
gpt4 key购买 nike

我有一个充满可变长度记录的字节缓冲区,其长度由记录的第一个字节决定。读取单个记录的 C 函数的简化版本

void mach_parse_compressed(unsigned char* ptr, unsigned long int* val)
{
if (ptr[0] < 0xC0U) {
*val = ptr[0] + ptr[1];
return;
}

*val = ((unsigned long int)(ptr[0]) << 24)
| ((unsigned long int)(ptr[1]) << 16)
| ((unsigned long int)(ptr[2]) << 8)
| ptr[3];
}

生成程序集(GCC 5.4 -O2 -fPIC on x86_64)首先在 ptr 加载四个字节,将第一个字节与 0xC0 进行比较,然后处理两个或四个字节。未定义的字节被正确丢弃,但为什么编译器首先认为加载四个字节是安全的?因为没有例如ptr 的对齐要求,它可能指向我们所知的未映射页面旁边的内存页面的最后两个字节,从而导致崩溃。

重现需要 -fPIC 和 -O2 或更高。

我是不是漏掉了什么?编译器这样做是否正确?我该如何解决这个问题?

我可以得到上面显示的 Valgrind/AddressSanitiser 错误或 mmap/mprotect 崩溃:

//#define HEAP
#define MMAP
#ifdef MMAP
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#elif HEAP
#include <stdlib.h>
#endif

void
mach_parse_compressed(unsigned char* ptr, unsigned long int* val)
{
if (ptr[0] < 0xC0U) {
*val = ptr[0] + ptr[1];
return;
}

*val = ((unsigned long int)(ptr[0]) << 24)
| ((unsigned long int)(ptr[1]) << 16)
| ((unsigned long int)(ptr[2]) << 8)
| ptr[3];
}

int main(void)
{
unsigned long int val;
#ifdef MMAP
int error;
long page_size = sysconf(_SC_PAGESIZE);
unsigned char *buf = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
unsigned char *ptr = buf + page_size - 2;
if (buf == MAP_FAILED)
{
perror("mmap");
return 1;
}
error = mprotect(buf + page_size, page_size, PROT_NONE);
if (error != 0)
{
perror("mprotect");
return 2;
}
*ptr = 0xBF;
*(ptr + 1) = 0x10;
mach_parse_compressed(ptr, &val);
#elif HEAP
unsigned char *buf = malloc(16384);
unsigned char *ptr = buf + 16382;
buf[16382] = 0xBF;
buf[16383] = 0x10;
#else
unsigned char buf[2];
unsigned char *ptr = buf;
buf[0] = 0xBF;
buf[1] = 0x10;
#endif
mach_parse_compressed(ptr, &val);
}

MMAP 版本:

Segmentation fault (core dumped)

使用 Valgrind:

==3540== Process terminating with default action of signal 11 (SIGSEGV)
==3540== Bad permissions for mapped region at address 0x4029000
==3540== at 0x400740: mach_parse_compressed (in /home/laurynas/gcc-too-wide-load/gcc-too-wide-load)
==3540== by 0x40060A: main (in /home/laurynas/gcc-too-wide-load/gcc-too-wide-load)

使用 ASan:

ASAN:SIGSEGV
=================================================================
==3548==ERROR: AddressSanitizer: SEGV on unknown address 0x7f8f4dc25000 (pc 0x000000400d8a bp 0x0fff884e56c6 sp 0x7ffc4272b620 T0)
#0 0x400d89 in mach_parse_compressed (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400d89)
#1 0x400b92 in main (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400b92)
#2 0x7f8f4c72082f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400c58 in _start (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400c58)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV ??:0 mach_parse_compressed

带有 Valgrind 的 HEAP 版本:

==30498== Invalid read of size 4
==30498== at 0x400603: mach_parse_compressed (mach0data_reduced.c:9)
==30498== by 0x4004DE: main (mach0data_reduced.c:34)
==30498== Address 0x520703e is 16,382 bytes inside a block of size 16,384 alloc'd
==30498== at 0x4C2DB8F: malloc (vg_replace_malloc.c:299)
==30498== by 0x4004C0: main (mach0data_reduced.c:24)

带有 ASan 的堆栈版本:

==30528==ERROR: AddressSanitizer: stack-buffer-overflow on address
0x7ffd50000440 at pc 0x000000400b63 bp 0x7ffd500003c0 sp
0x7ffd500003b0
READ of size 4 at 0x7ffd50000440 thread T0
#0 0x400b62 in mach_parse_compressed
CMakeFiles/innobase.dir/mach/mach0data_reduced.c:15
#1 0x40087e in main CMakeFiles/innobase.dir/mach/mach0data_reduced.c:34
#2 0x7f3be2ce282f in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400948 in _start
(/home/laurynas/obj-percona-5.5-release/storage/innobase/CMakeFiles/innobase.dir/mach/mach0data_test+0x400948)

谢谢

编辑: 添加了实际崩溃的 MMAP 版本,澄清了编译器选项

编辑 2: 报告为 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77673 .对于解决方法,在 if 语句之后插入编译器内存屏障 asm volatile("":::"memory"); 可以解决问题。谢谢大家!

最佳答案

恭喜!您发现了真正的编译器错误!

您可以使用 http://gcc.godbolt.org探索不同编译器和选项的汇编输出。

对于 x86 64 位 linux 的 gcc 版本 6.2,使用 gcc -fPIC -O2,您的函数编译为不正确代码:

mach_parse_compressed(unsigned char*, unsigned long*):
movzbl (%rdi), %edx
movl (%rdi), %eax ; potentially incorrect load of 4 bytes
bswap %eax
cmpb $-65, %dl
jbe .L5
movl %eax, %eax
movq %rax, (%rsi)
ret
.L5:
movzbl 1(%rdi), %eax
addl %eax, %edx
movslq %edx, %rdx
movq %rdx, (%rsi)
ret

您正确诊断了问题,mmap 示例提供了良好的回归测试。 gcc 过于努力地优化此功能,结果代码肯定是不正确的:对于大多数 X86 操作环境,从未对齐的地址读取 4 个字节是可以的,但读取超过数组末尾则不行。

如果不跨越 32 位或什至 64 位边界,编译器可能会假设读取数组末尾之后是可以的,但这个假设对于您的示例是不正确的。如果使用 malloc 分配的 block 足够大,则可能会导致崩溃。 mallocmmap 用于非常大的 block (默认情况下 >= 128KB IRCC)。

请注意,此错误 是在编译器 5.1 版中引入的。

clang 另一方面没有这个问题,但代码在一般情况下似乎效率较低:

#    @mach_parse_compressed(unsigned char*, unsigned long*)
mach_parse_compressed(unsigned char*, unsigned long*):
movzbl (%rdi), %ecx
cmpq $191, %rcx
movzbl 1(%rdi), %eax
ja .LBB0_2
addq %rcx, %rax
movq %rax, (%rsi)
retq
.LBB0_2:
shlq $24, %rcx
shlq $16, %rax
orq %rcx, %rax
movzbl 2(%rdi), %ecx
shlq $8, %rcx
orq %rax, %rcx
movzbl 3(%rdi), %eax
orq %rcx, %rax
movq %rax, (%rsi)
retq

关于c - 为什么编译器生成 4 字节负载而不是 1 字节负载,而更宽的负载可能会访问未映射的数据?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38821284/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com