看到标题大家可能会想,openssl和fPIC会有什么关系呢?这要从最近遇到的一个问题说起。由于openssl去年和今年被爆出了很多漏洞,而我们项目中用到了openssl较老的版本,所以需要重新编译openssl的新版本。这本应该是一间很简单的事,可是却遇到了问题。在RedHat 7.x平台上,编译之后的程序在跑的时候直接crash了,而且奇怪的是在RedHat AS4平台上没有问题。

废话不多说,上错误栈:

1
2
3
4
Program terminated with signal 11, Segmentation fault.
#0 0xf7779875c in sha1_block_data_order () from /opt/xxx/xxx/libxxx.so
#1 0xf7797926 in SHA1_Update () from /opt/xxx/xxx/libxxx.so
#2 0x00000000 in ?? ()

sha1_block_data_order为关键字经过一番搜索,发现网上也有遇到类似crash问题的人。给出的解释是,因为多线程调用,openssl在多线程调用时为了保证线程安全,需要在每个调用线程里明确设置两个callback函数。引用如下:

Is OpenSSL thread-safe?
Yes (with limitations: an SSL connection may not concurrently be used by multiple threads). On Windows and many Unix systems, OpenSSL automatically uses the multi-threaded versions of the standard libraries. If your platform is not one of these, consult the INSTALL file.

Multi-threaded applications must provide two callback functions to OpenSSL by calling CRYPTO_set_locking_callback() and CRYPTO_set_id_callback(), for all versions of OpenSSL up to and including 0.9.8[abc…]. As of version 1.0.0, CRYPTO_set_id_callback() and associated APIs are deprecated by CRYPTO_THREADID_set_callback() and friends. This is described in the threads(3) manpage.

本以为找着了问题的原因,然而在仔细搜索了代码之后发现我们已经设置了这两个callback。然后自己在redhat 7.3上面写了一个demo程序去调用,单线程的竟然也会crash!不过还好,本地可以复现。重新编译了一个debug版本的程序,用gdb一步一步的去跟。最后获取了比较详细的调用栈:

1
2
3
4
5
6
7
8
9
10
11
sha1_block_data_order ()
#0 SHA1_Update (c=0x80d7f60, data_=0xbfff5fe0, len=8) at ../md32_common.h:307
#1 0x40350e27 in update (ctx=0xbfff5fa0, data=0xbfff5fe0, count=8) at m_sha1.c:80
#2 0x4034879a in EVP_DigestUpdate (ctx=0xbfff5fa0, data=0xbfff5fe0, count=8)
at digest.c:244
#3 0x403e1bb3 in ssleay_rand_add (buf=0xbfff61c0, num=32, add=32) at md_rand.c:288
#4 0x40345cdc in RAND_add (buf=0xbfff61c0, num=32, entropy=32) at rand_lib.c:152
#5 0x403e298b in RAND_poll () at rand_unix.c:405
#6 0x403e24c3 in ssleay_rand_status () at md_rand.c:578
#7 0x40345de4 in RAND_status () at rand_lib.c:175
#8 0x40294a39 in _seedPRNG () at xxx.cpp:65

程序出错在程序给openssl发送随机种子时调用的sha1_block_data_order函数里,以下是此函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
$A="eax";
$B="ebx";
$C="ecx";
$D="edx";
$E="edi";
$T="esi";
$tmp1="ebp";
&function_begin("sha1_block_data_order");
&mov($tmp1,&wparam(0)); # SHA_CTX *c
&mov($T,&wparam(1)); # const void *input
&mov($A,&wparam(2)); # size_t num
&stack_push(16); # allocate X[16]
&shl($A,6);
&add($A,$T);
&mov(&wparam(2),$A); # pointer beyond the end of input
&mov($E,&DWP(16,$tmp1));# pre-load E
&set_label("loop",16);
# copy input chunk to X, but reversing byte order!
for ($i=0; $i<16; $i+=4)
{
&mov($A,&DWP(4*($i+0),$T));
&mov($B,&DWP(4*($i+1),$T));
&mov($C,&DWP(4*($i+2),$T));
&mov($D,&DWP(4*($i+3),$T));
&bswap($A);
&bswap($B);
&bswap($C);
&bswap($D);
&mov(&swtmp($i+0),$A);
&mov(&swtmp($i+1),$B);
&mov(&swtmp($i+2),$C);
&mov(&swtmp($i+3),$D);
}
&mov(&wparam(1),$T); # redundant in 1st spin
&mov($A,&DWP(0,$tmp1)); # load SHA_CTX
&mov($B,&DWP(4,$tmp1));
&mov($C,&DWP(8,$tmp1));
&mov($D,&DWP(12,$tmp1));
# E is pre-loaded
for($i=0;$i<16;$i++) { &BODY_00_15($i,@V); unshift(@V,pop(@V)); }
for(;$i<20;$i++) { &BODY_16_19($i,@V); unshift(@V,pop(@V)); }
for(;$i<40;$i++) { &BODY_20_39($i,@V); unshift(@V,pop(@V)); }
for(;$i<60;$i++) { &BODY_40_59($i,@V); unshift(@V,pop(@V)); }
for(;$i<80;$i++) { &BODY_20_39($i,@V); unshift(@V,pop(@V)); }
(($V[5] eq $D) and ($V[0] eq $E)) or die; # double-check
&mov($tmp1,&wparam(0)); # re-load SHA_CTX*
&mov($D,&wparam(1)); # D is last "T" and is discarded
&add($E,&DWP(0,$tmp1)); # E is last "A"...
&add($T,&DWP(4,$tmp1));
&add($A,&DWP(8,$tmp1));
&add($B,&DWP(12,$tmp1));
&add($C,&DWP(16,$tmp1));
&mov(&DWP(0,$tmp1),$E); # update SHA_CTX
&add($D,64); # advance input pointer
&mov(&DWP(4,$tmp1),$T);
&cmp($D,&wparam(2)); # have we reached the end yet?
&mov(&DWP(8,$tmp1),$A);
&mov($E,$C); # C is last "E" which needs to be "pre-loaded"
&mov(&DWP(12,$tmp1),$B);
&mov($T,$D); # input pointer
&mov(&DWP(16,$tmp1),$C);
&jb(&label("loop"));
&stack_pop(16);
&function_end("sha1_block_data_order");

可以发现其中用到了大量的寄存器,eax/ebx/ecx/edx等。后来突然联想到在编译openssl的时候我们加上了-fPIC选项,fPIC作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。那么会不会是-fPIC选项对于内嵌汇编代码的支持不好造成的呢。于是重新编译了一下openssl的库,问题果然解决了。
在问题解决之后又做了一些research,找到了一篇文章“gcc指定-fPIC编译的时候内嵌汇编需要注意的问题”。其中有提到:

gcc在生成位置无关代码的时候,内部使用了ebx作为基址寄存器。如果不使用内嵌汇编,那么gcc自然会帮助你维持ebx的值始终有效。但是如果使用了内嵌汇编,gcc常常就有点力不从心了,所以这时候,一定要自己留意保存好ebx的值。

所以造成sha1_block_data_order crash的原因应该就是其中用到了ebx,而且没有自己去保存好它。而对于位置无关的代码内部使用了ebx作为基址寄存器,所以就出现了Segmentation fault。可是还有一个问题,为什么redhat AS4平台上也用了fPIC而没有问题呢。我想这应该是因为老版本的编译器对于这种情况的handle不是很好。因为redhat 7.x的gcc版本确实比较低了些。
希望我跨过的这个坑会对后来人有所帮助。