每当程序中出现奇怪的问题时,人们总是习惯于抱怨所能想到的一切东西:kernel,C 库,编译器,链接器,其他人的代码,甚至硬件- 当然除了自己之外,然而,意料之中的是,绝大部分情况都是人们自己犯的错误. 所以当有人又在抱怨自己遇到到了一个奇怪的逻辑正确却运行错误的代码时,沉默的编译器和链接器以及uClibc 库被理所当然地成为了出气筒,可是,概率论又一次发挥了他神奇的统计作用–你还是掉在自己挖的叫“绝大部分”的区间里。
下面的这个例子是一个简化了的多线程同步的问题。该例子同时支持两种平台(arm/x86)和几个编译选项: V(verbose mode),S(static mode),R(librar选择顺序)。ARM板上的gcc和uClibc版本分别为gcc-2.95.3和uClibc-0.9.26;PC上的gcc和glibc版本分别为gcc-4.2和glic-2.7 。
对于大多数与开发板硬件无关的代码,我们可以首先在X86上验证逻辑的正确性。尽管x86 和No-mmu 的ARM7TDMI 相比有很大的差异,x86版的程序依然能够帮你检测出大多数逻辑上的错误(很显然不是全部,比如no-mmu 的栈overflow问题)。
程序是由两部分程序组成:一个简化的多线程程序simple.c和Makefile。
(1)缺省时ARCH为i386,可以通过make ARCH=arm 选择arm平台。
(2)x86下,可以通过make S=1 编译成静态连接的程序, arm 下由于所用格式为BFLT,静态选项无意义。
(3)缺省时,库链接顺序为-lpthread -lc;可以通过make R=1反转为-lc -lpthread。
(4)可以通过make V=1 打开verbose 编译器模式。
#include <stdio.h> #include <unistd.h> #include <pthread.h> pthread_mutex_t lock; pthread_cond_t cond; static void *func(void *args) { int count = 0; while (count++ < 10) { pthread_mutex_lock(&lock); pthread_cond_wait(&cond, &lock); fprintf(stderr, "Received the %d signal ./n", count); /* do something else here */ pthread_mutex_unlock(&lock); } return NULL; } int main(int argc, char *argv[]) { pthread_t pid; pthread_mutex_init(&lock, NULL); pthread_cond_init(&cond, NULL); pthread_create(&pid, NULL, func, NULL); /* do something else here */ pthread_cond_signal(&cond); /* do something else here */ /* Just for debug */ sleep(5); pthread_mutex_destroy(&lock); pthread_cond_destroy(&cond); return 0; }
# # Defines # # gcc version 4.2 for x86, glibc 2.7 ARCH := x86 # gcc version 2.95.3 for arm,uClibc 0.9.26 ifeq ($(ARCH), arm) CROSS := arm-elf- LDFLAGS += -Wl,-elf2flt="-s32768" endif CC := $(CROSS)gcc LD := $(CROSS)gcc AR := $(CROSS)ar TARGET := test LIBLIST := -lpthread -lc ifeq ($(R), 1) LIBLIST := -lc -lpthread endif # Static mode ifeq ($(S), 1) LDFLAGS += -static endif # Verbose mode ifeq ($(V), 1) LDFLAGS += -v endif OBJECTS := simple.o # # Commands # $(TARGET):$(OBJECTS) $(LD) $(LDFLAGS) -o $@ $^ $(LIBLIST) $(USERLIB):$(LIBOBJS) $(AR) rcu $(USERLIB) $^ %.o:%.c $(CC) $(CFLAGS) -c $< clean: @rm -f *.o *.a $(TARGET)
x86平台下:
• 动态,-lpthread -lc; make clean; make && ./test; 运行结果正确。
• 动态,-lc -lpthread; make clean; make R=1 && ./test; 运行结果正确。
• 静态,-lpthread -lc; make clean; make S=1 && ./test; 运行结果正确。
说明程序逻辑正确。
ARM平台下:
• case1: ARM, 静态, 库链接顺序-lc -lpthread; 结果异常:Received the signal出现了很多,事实上只发了一个signal。
• case2: ARM, 静态,库链接顺序-lpthread -lc; 结果运行正常。
我们首先来分析ARM case1 运行结果异常的原因:
[yin-laptop@2]$ make clean; make ARCH=arm R=1
arm-elf-gcc -c simple.c
arm-elf-gcc -Wl,-elf2flt="-s32768" -o test simple.o
-lc -lpthread
[yin-laptop@2]$ arm-elf-objdump -dj .text test.gdb | less
00008048 <func>: 8048: e1a0c00d mov ip, sp 804c: e92dd800 stmdb sp!, {fp, ip, lr, pc} 8050: e24cb004 sub fp, ip, #4 ; 0x4 8054: e24dd008 sub sp, sp, #8 ; 0x8 8058: e50b0010 str r0, [fp, -#16] 805c: e3a03000 mov r3, #0 ; 0x0 8060: e50b3014 str r3, [fp, -#20] 8064: e24b3014 sub r3, fp, #20 ; 0x14 8068: e5932000 ldr r2, [r3] 806c: e1a01002 mov r1, r2 8070: e2822001 add r2, r2, #1 ; 0x1 8074: e5832000 str r2, [r3] 8078: e3510009 cmp r1, #9 ; 0x9 807c: da000000 ble 8084 <func+0x3c> 8080: ea00000c b 80b8 <func+0x70> 8084: e59f0034 ldr r0, [pc, #34] ; 80c0 <func+0x78> 8088: eb000092 bl 82d8 <__pthread_return_0> 808c: e59f0030 ldr r0, [pc, #30] ; 80c4 <func+0x7c> 8090: e59f1028 ldr r1, [pc, #28] ; 80c0 <func+0x78> 8094: eb00008f bl 82d8 <__pthread_return_0> 8098: e59f3028 ldr r3, [pc, #28] ; 80c8 <func+0x80> 809c: e5930000 ldr r0, [r3] 80a0: e59f1024 ldr r1, [pc, #24] ; 80cc <func+0x84> 80a4: e51b2014 ldr r2, [fp, -#20] 80a8: eb0000cb bl 83dc <fprintf> 80ac: e59f000c ldr r0, [pc, #c] ; 80c0 <func+0x78> 80b0: eb000088 bl 82d8 <__pthread_return_0> 80b4: eaffffea b 8064 <func+0x1c> 80b8: e3a00000 mov r0, #0 ; 0x0 80bc: ea000003 b 80d0 <func+0x88> 80c0: 00018330 andeq r8, r1, r0, lsr r3 80c4: 00018348 andeq r8, r1, r8, asr #6 80c8: 00013ac8 andeq r3, r1, r8, asr #21 80cc: 000128b0 streqh r2, [r1], -r0 80d0: e91ba800 ldmdb fp, {fp, sp, pc} 000082d8 <__pthread_return_0>: 82d8: e1a0c00d mov ip, sp 82dc: e92dd800 stmdb sp!, {fp, ip, lr, pc} 82e0: e24cb004 sub fp, ip, #4 ; 0x4 82e4: e3a00000 mov r0, #0 ; 0x0 82e8: e91ba800 ldmdb fp, {fp, sp, pc}
__pthread_return_0 显然就是一个return 0的空函数,所以不停的会调用fprintf。那么为什么pthread_xxx 的调用会被解析成了__pthread_return0 呢?这个符号有时那里定义的呢?要想知道这些就只得去找uClibc的源代码了。
uClibc-0.9.26/libc/misc/pthread/weaks.c
weak_alias (__pthread_return_0, pthread_cond_wait) int __pthread_return_0 (void) { return 0; }
weak_alias 的定义在_install/include/feature.h,其实就是定义一个同类型的别名,同时把别名改为weak属性。
# define weak_alias(name, aliasname) _weak_alias (name, aliasname) # define _weak_alias(name, aliasname) / extern __typeof (name) aliasname __attribute__ ((weak, alias (#name)));
很显然库顺序的错误是导致这类错误的根本原因。做符号解析时,当func中的pthread_cond_wait之类的函数首先search libc.a,找到后就直接拷贝到test中,而真正的pthread_cond_wait 函数则被简单的丢弃。
那为什么uClibc 放这些没用的函数呢?主要是为了减小链接后的程序大小,当没有显示链接-lpthread 时就被当作单线程看待,因此也就没必要copy那些用不到的东西。再看以正确的库顺序(ARM case2)链接后的结构:
[yin-laptop@2]$ make clean; make ARCH=arm
arm-elf-gcc -c simple.c
arm-elf-gcc -Wl,-elf2flt="-s32768" -o test simple.o -lpthread -lc
00000274 <pthread_cond_wait>: 274: e1a0c00d mov ip, sp 278: e92dd870 stmdb sp!, {r4, r5, r6, fp, ip, lr, pc} 27c: e24cb004 sub fp, ip, #4 ; 0x4 280: e1a04000 mov r4, r0 284: e1a06001 mov r6, r1 288: e59f3210 ldr r3, [pc, #210] ; 4a0 <pthread_cond_wait+0x22c> 28c: e24dd00c sub sp, sp, #12 ; 0xc 290: e5933000 ldr r3, [r3] 294: e1a0200d mov r2, sp 298: e3530000 cmp r3, #0 ; 0x0 29c: 0a000005 beq 2b8 <pthread_cond_wait+0x44> 2a0: e1520003 cmp r2, r3 2a4: 3a000005 bcc 2c0 <pthread_cond_wait+0x4c> 2a8: e59f31f4 ldr r3, [pc, #1f4] ; 4a4 <pthread_cond_wait+0x230> 2ac: e5933000 ldr r3, [r3] 2b0: e1520003 cmp r2, r3 2b4: 2a000001 bcs 2c0 <pthread_cond_wait+0x4c> 2b8: e59f01e8 ldr r0, [pc, #1e8] ; 4a8 <pthread_cond_wait+0x234> 2bc: ea000009 b 2e8 <pthread_cond_wait+0x74> 2c0: e59f31e4 ldr r3, [pc, #1e4] ; 4ac <pthread_cond_wait+0x238> 2c4: e5933000 ldr r3, [r3] 2c8: e1520003 cmp r2, r3 2cc: 3a000004 bcc 2e4 <pthread_cond_wait+0x70> 2d0: e59f31d8 ldr r3, [pc, #1d8] ; 4b0 <pthread_cond_wait+0x23c> 4e8: e3a00000 mov r0, #0 ; 0x0 4ec: e91ba870 ldmdb fp, {r4, r5, r6, fp, sp, pc}
此时的pthread_cond_wait链接的是如假包换的正版函数而不是一个没用的备胎。
事实上-lc纯粹是多余的,没有必要的,手动运行:
[yin-laptop@2]$ arm-elf-gcc -c simple.c
arm-elf-gcc -v -o test simple.o -lpthread
Reading specs from /opt/toolchain-89/lib/
gcc-lib/arm-elf/2.95.3/specs
gcc version 2.95.3 20010315 (release)
/opt/toolchain-89/lib/gcc-lib/arm-elf/2.95.3/collect2
-X -o test
/opt/toolchain-89/lib/gcc-lib/arm-elf/2.95.3/crt0.o
/opt/toolchain-89/lib/gcc-lib/arm-elf/2.95.3/crti.o
-L/opt/toolchain-89/lib/gcc-lib/arm-elf/2.95.3
-L/opt/toolchain-89/arm-elf/lib simple.o
-lpthread -lgcc -lc -lgcc
可以发现gcc缺省已经为你加了-lc。本例中正是由于-lc 的泛滥才导致了这种运行时才会发现的链接错误。那为什么如此明显的-lpthread -lc 链接顺序会出错呢?事实上正是由于程序复杂度的降低才会使问题如此明显,实际的程序有几十个库之多,某些人为了符号解析的方便随意加了一个-lc,简单的问题就完全淹没在复杂性之中了。
由此可见,错误确实是源于无知而不是偶然和意外。