昨天在编译uClibc-ng中提取出来的malloc堆内存分配器时, 踩了一个小坑, 特此记录。
故事是这样的, 我在编译(开启了gcc的O2)相关代码为so之后对其进行测试, 测试到calloc函数时卡死。经过gdb等工具的调试, 我最终发现calloc函数编译生成的汇编代码是一个死循环。
原始C代码如下所示(即uClibc-ng的calloc实现):
void * calloc(size_t nmemb, size_t lsize) { void *result; size_t size=lsize * nmemb; /* guard vs integer overflow, but allow nmemb * to fall through and call malloc(0) */ if (nmemb && lsize != (size / nmemb)) { __set_errno(ENOMEM); return NULL; } if ((result=malloc(size)) != NULL) { memset(result, 0, size); } return result; }
死循环汇编代码如下所示:
0000000000000840 <calloc>: 840: 48 89 f1 mov %rsi,%rcx 843: 48 0f af cf imul %rdi,%rcx 847: 48 85 ff test %rdi,%rdi 84a: 74 0d je 859 <calloc+0x19> 84c: 31 d2 xor %edx,%edx 84e: 48 89 c8 mov %rcx,%rax 851: 48 f7 f7 div %rdi 854: 48 39 f0 cmp %rsi,%rax 857: 75 17 jne 870 <calloc+0x30> 859: be 01 00 00 00 mov $0x1,%esi 85e: 48 89 cf mov %rcx,%rdi 861: e9 8a fe ff ff jmpq 6f0 <calloc@plt> 866: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 86d: 00 00 00 870: 48 83 ec 08 sub $0x8,%rsp 874: e8 67 fe ff ff callq 6e0 <__errno_location@plt> 879: c7 00 0c 00 00 00 movl $0xc,(%rax) 87f: 31 c0 xor %eax,%eax 881: 48 83 c4 08 add $0x8,%rsp 885: c3 retq 886: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 88d: 00 00 00
注: calloc@plt的符号最后会被解析到840 calloc, 于是变为一个死循环。
进一步研究发现, gcc在开启O2的情况下, 会假设程序中调用的malloc、calloc、realloc以及free等函数是glibc的实现, 并对其做特定优化。在这个例子中, malloc和它后面的memset被优化为了对calloc的调用, 结合尾递归优化, 变为了死循环。
解决办法是在编译时加入”-fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc -fno-builtin-free”参数, 防止gcc的这类优化, 因为我们正在实现一个malloc库。
发表评论