本文共 5623 字,大约阅读时间需要 18 分钟。
最近在写项目,在涉及到内存分配时报标题中错误。该错误有以下两点神奇的特征:
MacOS
下用clang
编译后运行完全正常Ubuntu
下用gcc
编译后运行出上述断言错,但是在出错位置附近加puts("任意内容")
后,运行完全正常因为出错位置附近加puts("任意内容")
后,运行完全正常,且MacOS
下clang
编译后一切正常,初步推测该错误是由编译器不同引发。又由于断言在malloc
,该错误必定与内存分配有关。由于问题代码段在添加puts
等输出语句后问题消失,因此很难通过插入puts
的方法检测问题发生位置。不过在仔细检查代码后,终于将错误定位到了的get_pb
函数上。仔细阅读该函数以及相关结构体定义,错误原因终于水落石出:
...struct SIMPLE_PB { uint32_t struct_len, real_len; char target[];};typedef struct SIMPLE_PB SIMPLE_PB;...
...SIMPLE_PB* spb = malloc(struct_len + sizeof(uint32_t));...
原来,错误出在内存分配少了。可是为何mac下程序一切正常,在加入puts
后ubuntu下也正常了呢?
首先介绍以下两条规则:
8字节
对齐malloc
分配空间不满8字节
的倍数时,自动补齐。内存的对齐是由空间换效率的方法。
那么,上面的代码中假如struct_len
是8的倍数,加上sizeof(uint32_t)==4
,就必然不是8的倍数了。此时malloc
自动再加4字节补齐,恰好等于了struct SIMPLE_PB
中的两个成员uint32_t struct_len, real_len
的空间。 malloc
分配的内存还有overhead
信息(32字节),如果损坏该信息将报其它断言错struct_len=64+4=68
字节,加上4后实际传给malloc
是72字节,不满16的倍数,补为80字节(不含overhead
)因此,在没有任何检查的情况下,由于空间足够,程序应当运行正常。很显然gcc
在这里采用了断言进行了检查,因而报错,帮助我们发现了这个隐藏的错误。
puts
后ubuntu下也正常由于与编译器优化有关,我们只能从汇编代码上寻找原因了。
将问题代码单独提取出来,形成如下代码#include#include #include #include struct SIMPLE_PB { uint32_t struct_len, real_len; char target[];};typedef struct SIMPLE_PB SIMPLE_PB;struct DAT { char n[64]; uint32_t c;};typedef struct DAT DAT;static uint32_t read_num(FILE* fp) { uint8_t c; uint32_t n = 0; uint8_t i = 0; do { c = fgetc(fp); if(feof(fp)) return n; else n |= (c & 0x7f) << (7 * i++); } while((c & 0x80)); return n;}SIMPLE_PB* get_pb(FILE* fp) { uint32_t init_pos = ftell(fp); uint32_t struct_len = read_num(fp); if(struct_len > 1) { SIMPLE_PB* spb = malloc(struct_len + sizeof(uint32_t)); if(spb) { spb->struct_len = struct_len; spb->real_len = 0; char* p = spb->target; char* end = p + struct_len; memset(p, 0, struct_len); while(p < end) { uint32_t offset = read_num(fp); uint32_t data_len = read_num(fp); if(data_len > 0) fread(p, data_len, 1, fp); p += offset; } spb->real_len = ftell(fp) - init_pos; return spb; } } return NULL;}int main(){ SIMPLE_PB* spb = get_pb(fopen("dat.sp", "rb")); DAT* d = (DAT*)spb->target; printf("%d %d %s %u\n", spb->struct_len, spb->real_len, d->n, d->c); return 0; }
clang test.c -O3 -o test./test68 13 fumiama 9
结果一切正常。
gcc -O3 test.c -o test./test test: malloc.c:2401: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.Aborted (core dumped)
错误出现。
查看汇编代码如下:gcc -O3 -S test.c -o test.s
下面仅列出关键代码
.L2: cmpl $1, %r12d ; 比较struct_len jbe .L6 ; if(struct_len <= 1) return NULL movl %r12d, %ebp ; ebp = struct_len leaq 4(%rbp), %rdi ; 明确加了4 call malloc@PLT ; 调用实际按8字节对齐 testq %rax, %rax ; 比较spb movq %rax, 16(%rsp) ; 将分配的指针放入内存 je .L6 ; if(!spb) return NULL leaq 8(%rax), %r13 ; r13 = rax + 8指向了spb->target(char* p = spb->target) movl %r12d, (%rax) ; spb->struct_len = struct_len movl $0, 4(%rax) ; spb->real_len = 0(实际无法访问区域) xorl %esi, %esi ; esi = 0 movq %rbp, %rdx ; rdx = struct_len leaq 0(%r13,%rbp), %rax ; char* end = p + struct_len movq %r13, %rdi ; rdi = p movq %rax, %r15 ; r15 = end movq %rax, 8(%rsp) ; 保存end备用 call memset@PLT cmpq %r15, %r13 ; 比较p与end jnb .L7 ; p>=end退出循环 .p2align 4,,10 ; 进入while循环... .p2align 3
分析后发现,这段代码完全没有问题,也没有执行任何边界检查,因此问题并非出自这里。
使用gdb
调试后,发现问题出在printf
调用处: 也就是说,这个问题平时并不会出现,只有在调用printf
函数时,其内置的边界检测才会报错! 那么,让我们再来分析一下调用printf
时的汇编代码: .LC0: .string "rb".LC1: .string "dat.sp".LC2: .string "%d %d %s %u\n"....main:.LFB54: .cfi_startproc leaq .LC0(%rip), %rsi leaq .LC1(%rip), %rdi subq $8, %rsp .cfi_def_cfa_offset 16 call fopen@PLT ; 调用fopen movq %rax, %rdi call get_pb ; 调用get_pb movl 4(%rax), %ecx ; ecx = real_len movl 72(%rax), %r9d ; r9d = d->c leaq 8(%rax), %r8 ; r8 = n movl (%rax), %edx ; edx = struct_len leaq .LC2(%rip), %rsi ; rsi = &"%d %d %s %u\n" movl $1, %edi ; edi = 1 xorl %eax, %eax ; eax = 0 call __printf_chk@PLT ; 有检查的printf xorl %eax, %eax ; return 0 addq $8, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc
可见入参一切正常,在gdb
中进一步做断点,发现在执行printf
前,求值也没有任何问题,下面截图中的代码甚至将每个变量都分离表示然后传入printf
,但是仍然触发了断言。
| |
基于此,判断并不是printf
本身的问题,再结合断言由malloc
发出,因此尝试在printf
前加一条malloc
语句:
malloc(4);printf("%d %d %s %u\n", spb->struct_len, spb->real_len, d->n, d->c);
果然,程序执行到malloc
就已经报相同错误。
memory corruption
,下面让就我们来复现添加puts
运行成功的场景: puts("Magic!");SIMPLE_PB* spb = malloc(struct_len + sizeof(uint32_t));
于是,错误奇迹般地消失了:
那么让我们来看看加了puts
之后的汇编代码: .LC0: .string "Magic!" .text....L2: cmpl $1, %r12d jbe .L6 leaq .LC0(%rip), %rdi movl %r12d, %ebp call puts@PLT leaq 4(%rbp), %rdi call malloc@PLT...
可以推测,puts
申请并释放了一片内存,使后续的printf
可以直接使用puts
释放的内存块而无需经过内存完整性检测,因而调用成功。
printf
需要分配的块相同但是,我们并不知道这个大小具体是多少,也不知道其申请的内存块是否唯一。实际上,使用malloc(BUFSIZ)
申请一片内存并释放后,断言仍然会出现。那么,为了验证我们的构想,只有遍历所有可能的情况了。
fake_puts
函数如下: void fake_puts() { for(int i = 0; i < BUFSIZ; i++) { void* p = malloc(i); free(p); }}
使用该函数替换puts
fake_puts();SIMPLE_PB* spb = malloc(struct_len + sizeof(uint32_t));
禁用gcc
优化后编译运行,果然消除错误。
当然,要解决这个错误,只需要分配够空间即可。
SIMPLE_PB* spb = malloc(struct_len + 2 * sizeof(uint32_t));
这里提供程序用到的文件dat.sp
的编码,有兴趣的读者可以自己解码验证上述程序。
弐乶柕筩晛搐帄圀㴆
转载地址:http://uhmws.baihongyu.com/