C语言 GCC编译的程序运行报错 malloc.c:2401: sysmalloc: Assertion 的分析解决

博客详细分析了一个在不同操作系统和编译器环境下出现的内存分配错误,该错误在特定条件下表现得神秘莫测。作者通过逐步排查,发现错误源自于结构体内存分配不足,并解释了为什么在MacOS下和添加puts后Ubuntu下程序能够正常运行。最终,作者提出了修复方案,并探讨了puts如何通过改变内存状态避免了错误的发生。
摘要由CSDN通过智能技术生成

问题背景

最近在写项目CMoe-Counter,在涉及到内存分配时报标题中错误。该错误有以下两点神奇的特征:

  1. MacOS下用clang编译后运行完全正常
  2. Ubuntu下用gcc编译后运行出上述断言错,但是在出错位置附近加puts("任意内容")后,运行完全正常

错误分析

因为出错位置附近加puts("任意内容")后,运行完全正常,且MacOSclang编译后一切正常,初步推测该错误是由编译器不同引发。又由于断言在malloc,该错误必定与内存分配有关。由于问题代码段在添加puts等输出语句后问题消失,因此很难通过插入puts的方法检测问题发生位置。不过在仔细检查代码后,终于将错误定位到了simple-protobufget_pb函数上。仔细阅读该函数以及相关结构体定义,错误原因终于水落石出:

simple_protobuf.h

...
struct SIMPLE_PB {
    uint32_t struct_len, real_len;
    char target[];
};
typedef struct SIMPLE_PB SIMPLE_PB;
...

protobuf.c

...
SIMPLE_PB* spb = malloc(struct_len + sizeof(uint32_t));
...

原来,错误出在内存分配少了。可是为何mac下程序一切正常,在加入puts后ubuntu下也正常了呢?

问题解释

1. 为何mac下程序一切正常

首先介绍以下两条规则:

  1. 64位系统下,内存单元一定以8字节对齐
  2. malloc分配空间不满8字节的倍数时,自动补齐。

内存的对齐是由空间换效率的方法。
那么,上面的代码中假如struct_len是8的倍数,加上sizeof(uint32_t)==4,就必然不是8的倍数了。此时malloc自动再加4字节补齐,恰好等于了struct SIMPLE_PB 中的两个成员uint32_t struct_len, real_len的空间。

  1. 上面的这两条是经过简化的
  2. 由于内存的设计考量,实际上64位系统分配内存块是以16字节(128位)对齐
  3. malloc分配的内存还有overhead 信息(32字节),如果损坏该信息将报其它断言错
  4. 错误代码的情况中,struct_len=64+4=68字节,加上4后实际传给malloc是72字节,不满16的倍数,补为80字节(不含overhead

因此,在没有任何检查的情况下,由于空间足够,程序应当运行正常。很显然gcc在这里采用了断言进行了检查,因而报错,帮助我们发现了这个隐藏的错误。

2. 为何在加入puts后ubuntu下也正常

由于与编译器优化有关,我们只能从汇编代码上寻找原因了。
将问题代码单独提取出来,形成如下代码

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

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; 
}
1. 在Mac下编译
clang test.c -O3 -o test
./test
68 13 fumiama 9

结果一切正常。

2. 在Ubuntu下编译
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调用处:
gdb
也就是说,这个问题平时并不会出现,只有在调用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));

于是,错误奇迹般地消失了:
noerr
那么让我们来看看加了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释放的内存块而无需经过内存完整性检测,因而调用成功。
具体来说,该内存块需要满足以下条件:

  1. 刚刚被释放,未挪作他用
  2. 大小恰与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优化后编译运行,果然消除错误。
fake

解决方案

当然,要解决这个错误,只需要分配够空间即可。

SIMPLE_PB* spb = malloc(struct_len + 2 * sizeof(uint32_t));

附录

这里提供程序用到的文件dat.spbase16384编码,有兴趣的读者可以自己解码验证上述程序。

弐乶柕筩晛搐帄圀㴆
  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值