[转][转]PHP程序段错误分析

WBOY
リリース: 2016-06-06 20:13:32
オリジナル
1676 人が閲覧しました

来源:http://mp.weixin.qq.com/s?__biz=MjM5NzUwNDA5MA==mid=200518392idx=1sn=e42cd958ba83bd29aa3493952d8e16acscene=1#rd 基础架构的小伙伴们,大家好!之所以维护这样一个公众账号,主要有几个目的: 1. 日常工作中,我们帮业务团队排查过一些问题,有N


来源:http://mp.weixin.qq.com/s?__biz=MjM5NzUwNDA5MA==&mid=200518392&idx=1&sn=e42cd958ba83bd29aa3493952d8e16ac&scene=1#rd


基础架构的小伙伴们,大家好!之所以维护这样一个公众账号,主要有几个目的:

1. 日常工作中,我们帮业务团队排查过一些问题,有Nginx、PHP的,有三方扩展组件的,当然也少不了QBus、QConf、Bada 等咱们自己研发的,但往往同一个问题会多次被不同的业务遇到,为了让咱们少踩些坑,我们试过发邮件、挨个联系,现在又加入微信公众账号,目的就是希望能最大程度的把采坑过程传播出去,让大家少走弯路。另外,相比邮件而言,微信的沟通更灵活,一个棘手问题很可能在茶余饭后的一刹那就解决了:)

2. 我们会定期将大家关注的项目和功能开发进度汇报给亲们

3. 最重要的是,每个项目的发展都离不开大家的需求和建议,有问题请直接发信息给我们,我们将第一时间响应!

第一篇文章是普适的,相信你一定会遇到,请大家品味一下…

现象

addops反应,某业务的PHP,在运行一段时间后(几个月),会爆发大量段错误,除非重启php-fpm,
否则服务无法恢复。所有新启动的worker进程,都会立刻段错误crash掉。

问题排查

dmesg 信息

php-fpm22053: segfault at 2559 ip 000000398a6145b2 sp 00007fffad1d4b78 error 4 in ld-2.5.so398a600000+1c000

其实dmesg的日志,也可以给我们提供一些有效信息, 包括段错误的地址0x2559, 指令执行寄存器IP
0x 000000398a6145b2, 当前栈地址SP 0x00007fffad1d4b78, 错误号4,以及段错误发生在ld-2.5.so
中。

获取段错误具体位置

从上面的信息可以看出, 我们是在访问0x2559这个地址时出错, 显然0x2559是一个非法地址。由于段错误
发生在ld-2.5.so 中, 我们没有debug symbol信息,因此无法直接定位段错误在程序的哪一行。但通过IP
寄存器, 我们是可以定位到具体的汇编指令的。

objdump -d /lib64/ld-2.5.so > ld.asm
000000398a6145b0 <strcmp>:
  398a6145b0:   8a 07                   mov    (%rdi),%al
  398a6145b2:   3a 06                   cmp    (%rsi),%al
  398a6145b4:   75 0d                   jne    398a6145c3 <strcmp>
  398a6145b6:   48 ff c7                inc    %rdi
  398a6145b9:   48 ff c6                inc    %rsi
  398a6145bc:   84 c0                   test   %al,%al
  398a6145be:   75 f0                   jne    398a6145b0 <strcmp>
  398a6145c0:   31 c0                   xor    %eax,%eax
  398a6145c2:   c3                      retq
  398a6145c3:   b8 01 00 00 00          mov    $0x1,%eax
  398a6145c8:   b9 ff ff ff ff          mov    $0xffffffff,%ecx
  398a6145cd:   0f 42 c1                cmovb  %ecx,%eax
  398a6145d0:   c3                      retq</strcmp></strcmp></strcmp>
ログイン後にコピー

可以知道, 段错误发生在 cmp (%rsi),%al 指令。其中寄存器rdi, rsi 是上层传递的两个参数。

分析错误号含义

/*
 * Page fault error code bits:
 *
 *   bit 0 ==    0: no page found   1: protection fault
 *   bit 1 ==    0: read access     1: write access
 *   bit 2 ==    0: kernel-mode access  1: user-mode access
 *   bit 3 ==               1: use of reserved bit detected
 *   bit 4 ==               1: fault was an instruction fetch
 */
ログイン後にコピー

error 4 转化为二进制为100 即

bit 0 == 0 No page found
bit 1 == 1 Read access
bit 2 == 1 User mode
ログイン後にコピー

在用户态读访问时发生内存错误。

综上信息, 进程在读0x2559内存时(不是写),内存不可访问,导致段错误。

gdb 调试

由于故障现场保留,并且重启php-fpm前,可以一直重现。因此可以用gdb调试,看一下backtrace

(gdb) bt
#0  0x000000398a6145b2 in strcmp () from /lib64/ld-linux-x86-64.so.2
#1  0x000000398a607dc3 in _dl_map_object () from /lib64/ld-linux-x86-64.so.2
#2  0x000000398a610c4d in dl_open_worker () from /lib64/ld-linux-x86-64.so.2
#3  0x000000398a60ce96 in _dl_catch_error () from /lib64/ld-linux-x86-64.so.2
#4  0x000000398a61064c in _dl_open () from /lib64/ld-linux-x86-64.so.2
#5  0x000000398ab08ab0 in do_dlopen () from /lib64/libc.so.6
#6  0x000000398a60ce96 in _dl_catch_error () from /lib64/ld-linux-x86-64.so.2
#7  0x000000398ab08c17 in __libc_dlopen_mode () from /lib64/libc.so.6
#8  0x000000398aae3960 in __nss_lookup_function () from /lib64/libc.so.6
#9  0x000000398aae42a1 in __nss_next2 () from /lib64/libc.so.6
#10 0x000000398aae9e8e in gethostbyname2_r@@GLIBC_2.2.5 () from /lib64/libc.so.6
#11 0x000000398aabc523 in gaih_inet () from /lib64/libc.so.6
#12 0x000000398aabd62a in getaddrinfo () from /lib64/libc.so.6
#13 0x00007ffa786597f2 in Curl_getaddrinfo_ex () from /usr/local/curl/lib/libcurl.so.4
#14 0x00007ffa78654d34 in Curl_ipv4_resolve_r () from /usr/local/curl/lib/libcurl.so.4
#15 0x00007ffa78654d89 in Curl_getaddrinfo () from /usr/local/curl/lib/libcurl.so.4
#16 0x00007ffa78629955 in Curl_resolv () from /usr/local/curl/lib/libcurl.so.4
#17 0x00007ffa78629a41 in Curl_resolv_timeout () from /usr/local/curl/lib/libcurl.so.4
#18 0x00007ffa78639778 in resolve_server () from /usr/local/curl/lib/libcurl.so.4
#19 0x00007ffa7863b9e0 in create_conn () from /usr/local/curl/lib/libcurl.so.4
#20 0x00007ffa7863c209 in Curl_connect () from /usr/local/curl/lib/libcurl.so.4
#21 0x00007ffa7864b3eb in Curl_do_perform () from /usr/local/curl/lib/libcurl.so.4
#22 0x00007ffa78877683 in zif_curl_exec (ht=<value optimized out>, return_value=0x23cdd80, return_value_ptr=<value optimized out>, this_ptr=<value optimized out>,
    return_value_used=<value optimized out>) at /usr/src/redhat/BUILD/php-5.3.27/ext/curl/interface.c:2320
#23 0x000000000058b996 in zend_do_fcall_common_helper_SPEC ()
#24 0x000000000058b0ae in execute ()
#25 0x0000000000563109 in zend_execute_scripts ()
#26 0x000000000050fa28 in php_execute_script ()
#27 0x00000000005f53c4 in main ()</value></value></value></value>
ログイン後にコピー

域名解析和NSS机制

从backtrace可以看到,调用是从Curl_do_perform开始, 然后Curl_resolv进行域名解析。有的
同学看到Curl_resolv_timeout调用,猜测是域名解析超时,导致出现段错误。其实这是不对的,Curl_resolv_timeout
的含义实际是带有超时的域名解析调用,即可以设置多少时间后如果还不能解析,则超时。其函数原型为:

int Curl_resolv_timeout(struct connectdata *conn,                        const char *hostname,                        int port,
                        struct Curl_dns_entry **entry,                        long timeoutms)
ログイン後にコピー

可以看到, 最后一个参数timeoutms可以设置超时时间。从backtrace看,这个函数还没有返回,说明
域名解析没有超时,也就是说,段错误不是因为域名解析超时引起。

从backtrace继续往上看,最终离开libcurl.so, 进入libc.so, 调用gethostbyname2_r@@GLIBC_2.2.5。
再上面调用__nss_next2,这里其实是glibc的NSS机制。

NSS(Name Service Switch机制解决了unix下
一个服务可能有多种实现的问题, 比如域名解析,可以从/etc/hosts文件解析, 也可以通过DNS进行解析。
而glibc对外的接口只有一个gethostbyname,那么当调用gethostbyname时, 应该由谁来提供服务呢?
这在/etc/nsswitch.conf中可以配置, 比如:

hosts:      files dns
ログイン後にコピー

这里的意思是,对于域名解析服务,先由文件提供(/etc/hosts),如果不能解析,则从dns服务提供。
如果域名由dns服务提供,则程序需要加载libnss_dns.so。从上面do_dlopen, _dl_open, _dl_map_object
等调用可以猜测, 应该是加载动态链接库的过程。

加载动态链接库

从backtrace可以看到, 是_dl_map_object调用strcmp时,出现段错误,我们先看下_dl_map_object
的相关源码

/* Map in the shared object file NAME.  */
struct link_map *
internal_function
_dl_map_object (struct link_map *loader, const char *name, int preloaded,        int type, int trace_mode, int mode, Lmid_t nsid)
{
... ...
 /* Look for this name among those already loaded.  */  for (l = GL(dl_ns)[nsid]._ns_loaded; l; l = l->l_next)
    {
    ... ...
    ... ...
      soname = ((const char *) D_PTR (l, l_info[DT_STRTAB])
            + l->l_info[DT_SONAME]->d_un.d_val);      if (strcmp (name, soname) != 0)        continue;
    ... ...
    ... ...
    }
... ...
}
ログイン後にコピー

无关代码已经省略掉,从代码及注释可以看到,glibc在加载so时,先遍历当前加载过的so,如果发现已经
加载过了,则不再加载。这里的·name·是要加载的so的名字,·soname·是被遍历的so的名字,这两个名字
被传递给strcmp进行比较,导致段错误。

那么,name和soname必然有一个是非法地址。因为段错误我们可以一直重现,因此用gdb调试并执行(run),
到最终出现段错误。查看strcmp的两个参数的地址。由于没有调试符号, 我们只能从汇编来看,strcmp
的两个参数通过寄存器rsi, rdi传递。

info register rsi rdi
rsi            0x2559   9561
rdi            0x7fff24058700   140733797730048
ログイン後にコピー

可见,是寄存器rsi中的地址有问题(0x2559),那么rsi对应的是name还是soname呢?为了验证这个问题,
我手动写一个小程序实现一个简单的函数,并接收两个参数,gdb调试发现,第一个参数由rdi传递,第二
个参数由rsi传递。因此可以合理推测rsi对应soname。这个问题在后面的调试过程中,可以会做相关验证。

从上面的信息可以看到,glibc在加载动态链接库时,需要先判断是否已加载过,判断的方法就是比较动态
链接库的名字soname。由于某个已加载的soname是非法地址, 导致调用strcmp时读内存非法,发生段错误。

哪个so的soname非法?

到这里,我猜测是否程序溢出,覆盖了某个动态链接库的soname,可暂时没有可能导致溢出的任何线索。
我只能暂时尽量收集一下能得到的信息,因此我想找出到底是哪个so的soname是非法地址。至于这个so
是一个还是多个,找到后是否有用,我也不清楚。

为了找到非法soname对应的so,再次用gdb调试。分析得到soname的汇编指令:

0x000000398a607d9c <_dl_map_object>:        mov    0xb0(%rbx),%rdx
0x000000398a607da3 <_dl_map_object>:        test   %rdx,%rdx
0x000000398a607da6 <_dl_map_object>:        je     0x398a607bf4 <_dl_map_object>
0x000000398a607dac <_dl_map_object>:        mov    0x68(%rbx),%rax
0x000000398a607db0 <_dl_map_object>:        mov    0x8(%rdx),%r12
0x000000398a607db4 <_dl_map_object>:        mov    %rbp,%rdi
0x000000398a607db7 <_dl_map_object>:        add    0x8(%rax),%r12
0x000000398a607dbb <_dl_map_object>:        mov    %r12,%rsi
0x000000398a607dbe <_dl_map_object>:        callq  0x398a6145b0 <strcmp>
0x000000398a607dc3 <_dl_map_object>:        test   %eax,%eax
0x000000398a607dc5 <_dl_map_object>:        jne    0x398a607bf4 <_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></strcmp></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object></_dl_map_object>
ログイン後にコピー

可以看到,在调用strcmp之前,执行指令

mov %r12, %rsi
ログイン後にコピー

,也就是将soname的地址放到rsi
寄存器中。那么在这一句下断点并打印寄存器rsi, 如果发现是0x2559, 则说明导致了由问题的so。
经过一段跟踪调试,终于执行到soname的值为0x2559对应的so。那么当前的so是哪个呢?soname是非法
地址, 能否从其他方式得到so的名字呢?
经过分析代码, 发现link_map中有一个变量l_name 也保存了so的名字

struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */
    ElfW(Addr) l_addr;      /* Base address shared object is loaded at.  */    char *l_name;       /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;        /* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
    ... ...
  }
ログイン後にコピー

l_name 所在struct link_map结构体的偏移为8,通过分析汇编得知link_map对象地址保存在rbx,

(gdb)  x /30x 0xd7dc20 (link_map) $rbx
0xd7dc20:       0x00007f75f5ba1000      0x0000000000d7dbf0
0xd7dc30:       0x00007f75f5db7130      0x0000000000f24ca0
... ...
ログイン後にコピー

rbx中偏移为8的地址为0x0000000000d7dbf0, x /s 0x0000000000d7dbf0可以看到,其内容为:

(gdb) x /s 0x0000000000d7dbf0
0xd7dbf0:        "/usr/local/lib/libzookeeper_mt.so.2"
ログイン後にコピー

从而可以的得知,动态链接库libzookeeper_mt.so.2对应的soname为0x2559,是非法地址,导致在
strcmp参与比较时访问非法地址,发生段错误。

0x2559 是从哪里来?

我们虽然找到了libzookeeper_mt.so.2对应的soname是非法地址, 但这并不说明是libzookeeper_mt.so.2
除了问题。soname是怎么得来的呢? 是malloc分配内存并赋值的,还是指向了libzookeeper_mt.so.2
mmap进内存后的一个地址?
从代码

soname = ((const char *) D_PTR (l, l_info[DT_STRTAB])
      + l->l_info[DT_SONAME]->d_un.d_val);
ログイン後にコピー

猜测,DT_STRTAB应该表示ELF文件中的String table,DT_SONAME应该是String table的一个偏移。
因此我猜测soname实际是指向mmap内存的一个地址。为了验证这个问题, 我查了下ELF文件的格式。发现

http://refspecs.linuxbase.org/elf/elf.pdfDT_SONAME
This element holds the string table offset of a null-terminated string, giving
the name of the shared object. The offset is an index into the table recorded
in the DT_STRTAB entry. See "Shared Object Dependencies" below formore information about these names.
DT_STRTAB
This element holds the address of the string table, described in Chapter 1.
Symbol names, library names, and other strings reside in this table.
ログイン後にコピー

可以看到DT_STRTAB是String table在文件中的地址, DT_SONAME是soname在String table的偏移。
从这两个偏移,就可以定位soname的偏移地址。跟上面代码soname的获取方式一致,这说明了soname应该
是指向mmap映射的一块地址。

DT_STRTAB, DT_SONAME 的内容实际都存在l_info中, 可以用gdb看一下:

(gdb) x /10xg 0x7f75f5db71a0 (l_info[DT_STRTAB]) $rax
0x7f75f5db71a0: 0x0000000000000005      0x0000000000001898
0x7f75f5db71b0: 0x0000000000000006      0x0000000000000560
0x7f75f5db71c0: 0x000000000000000a      0x0000000000000cf8
0x7f75f5db71d0: 0x000000000000000b      0x0000000000000018
0x7f75f5db71e0: 0x0000000000000003      0x0000000000216350
(gdb) x /10x 0x00007f75f5db7160 (l_info[DT_SONAME]) $rdx
0x7f75f5db7160: 0x000000000000000e      0x0000000000000cc1
0x7f75f5db7170: 0x000000000000000c      0x0000000000003728
0x7f75f5db7180: 0x000000000000000d      0x00000000000110a8
0x7f75f5db7190: 0x000000006ffffef5      0x0000000000000158
0x7f75f5db71a0: 0x0000000000000005      0x0000000000001898
ログイン後にコピー

这里0x0000000000001898 对应DT_STRTAB, 0x0000000000000cc1 对应DT_SONAME, 两者相加

0x0000000000001898 + 0x0000000000000cc1 = 0x2559
ログイン後にコピー

可以看到,DT_STRTAB应该对应一个指向字符串表的地址,而这里是0x1898, 显然是一个非法地址。那
说明DT_STRTAB对应的地址本身就有问题了。

验证文件soname

既然soname实际指向mmap的地址, 那么从文件中应该也可以找到这个地址。看一下libzookeepr的映射
布局

7f75f5ba1000-7f75f5bb7000 r-xp 00000000 ca:41 15288827                   /usr/local/lib/libzookeeper_mt.so.2.0.0
7f75f5bb7000-7f75f5db7000 ---p 00016000 ca:41 15288827                   /usr/local/lib/libzookeeper_mt.so.2.0.0
7f75f5db7000-7f75f5db8000 rw-p 00016000 ca:41 15288827                   /usr/local/lib/libzookeeper_mt.so.2.0.0
ログイン後にコピー

这里有3段地址, 第一段对应文件的.text段,也就是代码段,从权限上可以看到, r-xp 可读,可执行,私有。
第二段没有任何读写执行的权限,我也不知道这一段对应什么。第三段具有读写权限,实际对应ELF中的
.dynamic 段。
顾名思义,.dynamic段中包含了一些动态的东西,比如地址,偏移等,这些地址需要根据so加载的地址
relocate到真正的内存地址,其中DT_STRTAB就保存在.dynamic 段。

[root@hao_hao03v ~]# objdump -s -j .dynamic /usr/local/lib/libzookeeper_mt.so.2.0.0
/usr/local/lib/libzookeeper_mt.so.2.0.0:     file format elf64-x86-64
Contents of section .dynamic:
 216130 01000000 00000000 9d0c0000 00000000  ................
 216140 01000000 00000000 a70c0000 00000000  ................
 216150 01000000 00000000 b70c0000 00000000  ................
 216160 0e000000 00000000 c10c0000 00000000  ................
 216170 0c000000 00000000 28370000 00000000  ........(7......
 216180 0d000000 00000000 a8100100 00000000  ................
 216190 f5feff6f 00000000 58010000 00000000  ...o....X.......
 2161a0 05000000 00000000 98180000 00000000  ................
 2161b0 06000000 00000000 60050000 00000000  ........`.......
 2161c0 0a000000 00000000 f80c0000 00000000  ................
 2161d0 0b000000 00000000 18000000 00000000  ................
 2161e0 03000000 00000000 50632100 00000000  ........Pc!.....
 2161f0 02000000 00000000 680a0000 00000000  ........h.......
 216200 14000000 00000000 07000000 00000000  ................
 216210 17000000 00000000 c02c0000 00000000  .........,......
 216220 07000000 00000000 b0270000 00000000  .........'......
 216230 08000000 00000000 10050000 00000000  ................
 216240 09000000 00000000 18000000 00000000  ................
 216250 feffff6f 00000000 30270000 00000000  ...o....0'......
 216260 ffffff6f 00000000 03000000 00000000  ...o............
 216270 f0ffff6f 00000000 90250000 00000000  ...o.....%......
 216280 f9ffff6f 00000000 2e000000 00000000  ...o............
 216290 00000000 00000000 00000000 00000000  ................
 2162a0 00000000 00000000 00000000 00000000  ................
 2162b0 00000000 00000000 00000000 00000000  ................
 2162c0 00000000 00000000 00000000 00000000  ................
 2162d0 00000000 00000000 00000000 00000000  ................
ログイン後にコピー

其中

2161a0 05000000 00000000 98180000 00000000
ログイン後にコピー

对应DT_STRTAB, 第一个字节
0x05代表后面是一个地址(参考ELF),及98180000 00000000是一个地址,对应Little Endian的值
为 0x0000000000001898
从这里可以看到,DT_STRTAB地址,在文件中也是0x1898, 跟gdb的内容一致。那么最终soname的地址
0x2559, 是否也对应文件中的soname呢?

hexdump -C /usr/local/lib/libzookeeper_mt.so.2.0.0
... ...
00002540  69 62 70 74 68 72 65 61  64 2e 73 6f 2e 30 00 6c  |ibpthread.so.0.l|
00002550  69 62 63 2e 73 6f 2e 36  00 6c 69 62 7a 6f 6f 6b    |ibc.so.6.libzook|
00002560  65 65 70 65 72 5f 6d 74  2e 73 6f 2e 32 00 47 4c   |eeper_mt.so.2.GL|
... ...
ログイン後にコピー

可以看到文件偏移0x2559确实对应libzookeeper_mt.so.2这个字符串。说明gdb的地址实际正确的文件
偏移,只是没有重定位到正确的内存地址。

当前进展及总结

到目前为止, 我们可以看到, 是libzookeeper_mt_so.2 对应的soname地址,没有重定位到内存地址
空间的地址, 导致在加载新的so时,调用strcmp对比soname跟新加载的name,造成非法访问地址0x2559,
发生段错误。

但是,这种情况是如何造成的? 0x2559 为什么没有被重定位为进程地址空间的内存地址?
1. 基本排除溢出的情况, 因为 0x2559 是正确的文件偏移, 而不是一个随机值。
2. 排除文件STRTAB损毁,或硬盘错误, 因为新启动的进程没有出现段错误。
3. 不太可能是glibc的bug, 否则这种问题会很普遍, 但我从网上没有找到任何类似的问题。
4. 不太可能是mmap的问题,道理同3

验证mmap机制

我突然想到, 在用cat 重定向覆盖so文件时, 虽然so内容完全一样, 但仍然会发生段错误。是否是
so文件被覆盖,导致mmap出现问题?
从man mmap 可以看到:

MAP_PRIVATE
Create a private copy-on-write mapping.  Stores to the region do not affect the
original file. It is unspecified whether changes made to the file after the mmap()
call are visible in the mapped region.
ログイン後にコピー

对应private 方式映射的文件, 内存中的改变,是不会写会原文件的,如果原文件发生改变,那么行为
是未定义的。

因此我做了一个实验:

1. 将php-fpm.conf 复制到php-fpm-debug.conf , 并修改监听端口到9001, 将pm.max_requests
设置为1 ,这样做的目的是为了每次请求都让master进程新fork一个worker进程。(因为从前面的分析,
其实master进程本身就有问题)
2. 启动sbin/php-fpm –y etc/php-fpm-debug.conf
3. 修改nginx 配置文件,指向我新启动的php-fpm , 并reload
4. Curl 访问, 响应正确返回,php-fpm也没有段错误,说明我们的debug环境工作正常。
5. cp /usr/local/lib/libzookeeper_mt.so.2.0.0 zk.so
6. cat zk.so > /usr/local/lib/libzookeeper_mt.so.2.0.0
7. curl 访问出错, 查看dmesg, 出现段错误

5,6,7的目的实际就是验证当我php-fpm运行过程中, 直接替换/usr/local/lib/libzookeeper_mt.so.2.0.0 的内容,
验证是否出现段错误。验证结果表明,会出现段错误,并跟我们遇到的问题的表现完全一样:

php-fpm[22053]: segfault at 2559 ip 000000398a6145b2 sp 00007fffad1d4b78 error 4 in ld-2.5.so[398a600000+1c000]
ログイン後にコピー

这说明了,linux的mmap在原文件修改了之后,会重新将内存中修改后的值覆盖为文件中的值。也即是
0x2559 虽然被重定位为正确的内存地址, 但如果libzookeeper_mt.so.2.0.0 被覆盖过, 则重定向
的地址又变回 0x2559,从而导致段错误。

那么, 是不是这个原因呢?
通过跟addops沟通, 发现qconf,跟kafka都用到了libzookeeper, 并且kafka安装时,可能是用脚
本直接安装的, 脚本在安装so时, 用了cp直接拷贝, 造成覆盖/usr/local/lib/libzookeeper_mt.so.2.0.0
于是我把实验的cat zk.so > /usr/local/lib/libzookeeper_mt.so.2.0.0 换为
cp zk.so /usr/local/lib/libzookeeper_mt.so.2.0.0, 问题同样出现。

流程推演

因此, 段错误的根本原因就是cp 直接覆盖了/usr/local/lib/libzookeeper_mt.so.2.0.0, 导致
mmap后修改的值被重新覆盖,也就是STRTAB的地址由内存地址空间的地址,覆盖为了文件偏移,从而导
致访问这个地址时,出现段错误。

我们来重新推演一下整个流程:

1. php-fpm 启动, 一切正常,没有段错误, master 进程此时是正常的。
2. worker 处理请求, 其中会调用gethostbyname2_r 进行域名解析, 导致加载libnss_dns.so.2,
这时候没有问题,加载正常。(关于为什么进程启动时没有加载libnss_dns.so.2 这应该跟glibc的NSS机制有关)
3. cp 覆盖掉/usr/local/lib/libzookeeper_mt.so.2.0.0 , 此时, 所有 php-fpm 进程都变为异常。
4. 由于worker进程已经加载了libnss_dns.so.2, 因此一直没有问题,段错误没有被触发。
5. 随着php-fpm不断运行, 由于进程管理为dynamic 方式, 在空闲时会有一些worker进程被kill,
比较忙时,有会有一些worker进程被启动。
6. 新启动的worker进程由于没有加载过libnss_dns.so.2, 因此在做域名解析时,需要加载libnss_dns.so.2,
但由于其mmap的/usr/local/lib/libzookeeper_mt.so.2.0.0 的STRTAB地址已经有问题,触发段错误。
7. 新的worker进程不断增多,导致段错误出现越来越多!

整个流程完全可以解释我们看到的现象。

解决方案

动态链接库文件时不能直接替换的, 否则会引发各种crash,正常的替换可以:
1. 直接mv, mv zk.so /usr/local/lib/libzookeeper_mt.so.2.0.0
2. 先mv走,再cp, mv /usr/local/lib/libzookeeper_mt.so.2.0.0 /usr/local/lib/libzookeeper_mt.so.2.0.0.bak , cp zk.so /usr/local/lib/libzookeeper_mt.so.2.0.0
3. 先rm掉, 再cp, rm /usr/local/lib/libzookeeper_mt.so.2.0.0; cp zk.so /usr/local/lib/libzookeeper_mt.so.2.0.0
4. Linux提供了安装命令, install zk.so /usr/local/lib/libzookeeper_mt.so.2.0.0

其本质就是,不要在原文件(inode)上直接修改文件内容。

参考

1. man mmap
2. http://stackoverflow.com/questions/4453781/what-happens-when-you-overwrite-a-memory-mapped-executable
3. http://www.iecc.com/linker/linker10.html
4. http://downloads.openwatcom.org/ftp/devel/docs/elf-64-gen.pdf
5. http://www.gnu.org/software/libc/manual/html_mono/libc.html#Name-Service-Switch


作者:heiyeshuwu 发表于2014-10-9 17:48:50 原文链接

阅读:601 评论:0 查看评论

ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のおすすめ
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート