5年+开发经验必须掌握的定位Linux程序崩溃的手段

文摘   2024-05-27 08:20   江苏  

maps

/proc/${pid}/maps 文件是 Linux 操作系统中一个非常重要的文件,它记录了一个进程的内存映射信息,其中包括了进程加载的库、堆栈、数据和代码段等等。该文件对于进行系统调试和性能优化等工作非常有帮助。

一句话概括 maps 的作用,即用来描述程序运行时进程的内存分布情况。总共包括六列,每列及其含义如下:

名字含义
address本段在虚拟内存中的地址范围。
perms本段的权限,r-读,w-写,x-执行, p-私有,s-共享。
offset即本段映射地址在文件中的偏移。
dev主设备号与次设备号:所映射的文件所属设备的设备号。
inode文件索引节点号。
pathname映射的文件名。 
对有名映射而言,是映射的文件名。
对匿名映射来说,是此段内存在进程中的作用。

注意pathname 可能为空,表示匿名映射;对于匿名内存区域,它通常为[stack] 或 [heap]

[stack] 表示本段内存作为栈来使用,其中:

  • [stack] 表示初始进程(主线程)的stack ;

  • [stack:<tid>] 表示线程 ID 为 tid 的 stack 。对应于 /proc/[pid]/task/[tid]/ 路径。

[heap] 作为堆来使用。

其他情况则为空。

对于有文件名的内存区间而言:

  • 属性为r--p表示存放的是rodata;

  • 属性为rw-p存放的是bssdata;

  • 属性为r-xp表示存放的是text数据。

没有文件名的内存区间则表示用 mmap 映射的匿名空间。

举个例子,如下是一个进程号为 227776 的进程 bash 的内存分布情况,即 /proc/227776/maps 文件中的内容:

       address            perms offset   dev   inode      pathname
558d8d991000-558d8da99000 r-xp 00000000 08:01 25390215 /usr/bin/bash #text
558d8dc98000-558d8dc9c000 r--p 00107000 08:01 25390215 /usr/bin/bash #rodata
558d8dc9c000-558d8dca5000 rw-p 0010b000 08:01 25390215 /usr/bin/bash #bss data
558d8dca5000-558d8dcaf000 rw-p 00000000 00:00 0
558d8f214000-558d8f3ff000 rw-p 00000000 00:00 0 [heap] #堆
7f3bfec74000-7f3bfeeec000 r--p 00000000 08:01 16940128 /usr/lib/locale/en_US.utf8/LC_COLLATE
7f3bfeeec000-7f3bfeef7000 r-xp 00000000 08:01 696125 /usr/lib64/libnss_files-2.28.so
7f3bfeef7000-7f3bff0f7000 ---p 0000b000 08:01 696125 /usr/lib64/libnss_files-2.28.so
7f3bff0f7000-7f3bff0f8000 r--p 0000b000 08:01 696125 /usr/lib64/libnss_files-2.28.so
7f3bff0f8000-7f3bff0f9000 rw-p 0000c000 08:01 696125 /usr/lib64/libnss_files-2.28.so
7f3bff0f9000-7f3bff0ff000 rw-p 00000000 00:00 0
7f3bff0ff000-7f3bff9d3000 r--s 00000000 08:01 25673372 /var/lib/sss/mc/passwd
7f3bff9d3000-7f3bff9dd000 r-xp 00000000 08:01 545531 /usr/lib64/libnss_sss.so.2
7f3bff9dd000-7f3bffbdc000 ---p 0000a000 08:01 545531 /usr/lib64/libnss_sss.so.2
7f3bffbdc000-7f3bffbdd000 r--p 00009000 08:01 545531 /usr/lib64/libnss_sss.so.2
7f3bffbdd000-7f3bffbde000 rw-p 0000a000 08:01 545531 /usr/lib64/libnss_sss.so.2
7f3bffbde000-7f3bffd99000 r-xp 00000000 08:01 236012 /usr/lib64/libc-2.28.so
7f3bffd99000-7f3bfff99000 ---p 001bb000 08:01 236012 /usr/lib64/libc-2.28.so
7f3bfff99000-7f3bfff9d000 r--p 001bb000 08:01 236012 /usr/lib64/libc-2.28.so
7f3bfff9d000-7f3bfff9f000 rw-p 001bf000 08:01 236012 /usr/lib64/libc-2.28.so
7f3bfff9f000-7f3bfffa3000 rw-p 00000000 00:00 0
7f3bfffa3000-7f3bfffa6000 r-xp 00000000 08:01 696119 /usr/lib64/libdl-2.28.so
7f3bfffa6000-7f3c001a5000 ---p 00003000 08:01 696119 /usr/lib64/libdl-2.28.so
7f3c001a5000-7f3c001a6000 r--p 00002000 08:01 696119 /usr/lib64/libdl-2.28.so
7f3c001a6000-7f3c001a7000 rw-p 00003000 08:01 696119 /usr/lib64/libdl-2.28.so
7f3c001a7000-7f3c001d0000 r-xp 00000000 08:01 235904 /usr/lib64/libtinfo.so.6.1
7f3c001d0000-7f3c003cf000 ---p 00029000 08:01 235904 /usr/lib64/libtinfo.so.6.1
7f3c003cf000-7f3c003d3000 r--p 00028000 08:01 235904 /usr/lib64/libtinfo.so.6.1
7f3c003d3000-7f3c003d4000 rw-p 0002c000 08:01 235904 /usr/lib64/libtinfo.so.6.1
7f3c003d4000-7f3c00402000 r-xp 00000000 08:01 235980 /usr/lib64/ld-2.28.so
7f3c00599000-7f3c005ec000 r--p 00000000 08:01 691495 /usr/lib/locale/C.utf8/LC_CTYPE
7f3c005ec000-7f3c005ed000 r--p 00000000 08:01 17146513 /usr/lib/locale/en_US.utf8/LC_NUMERIC
7f3c005ed000-7f3c005ee000 r--p 00000000 08:01 25390049 /usr/lib/locale/en_US.utf8/LC_TIME
7f3c005ee000-7f3c005ef000 r--p 00000000 08:01 26023510 /usr/lib/locale/en_US.utf8/LC_MONETARY
7f3c005ef000-7f3c005f0000 r--p 00000000 08:01 25390064 /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES
7f3c005f0000-7f3c005f1000 r--p 00000000 08:01 9432867 /usr/lib/locale/en_US.utf8/LC_PAPER
7f3c005f1000-7f3c005f2000 r--p 00000000 08:01 16801390 /usr/lib/locale/en_US.utf8/LC_NAME
7f3c005f2000-7f3c005f3000 r--p 00000000 08:01 25390072 /usr/lib/locale/en_US.utf8/LC_ADDRESS
7f3c005f3000-7f3c005f4000 r--p 00000000 08:01 26023511 /usr/lib/locale/en_US.utf8/LC_TELEPHONE
7f3c005f4000-7f3c005f9000 rw-p 00000000 00:00 0
7f3c005f9000-7f3c005fa000 r--p 00000000 08:01 26009535 /usr/lib/locale/en_US.utf8/LC_MEASUREMENT
7f3c005fa000-7f3c00601000 r--s 00000000 08:01 236038 /usr/lib64/gconv/gconv-modules.cache
7f3c00601000-7f3c00602000 r--p 00000000 08:01 26009534 /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION
7f3c00602000-7f3c00603000 r--p 0002e000 08:01 235980 /usr/lib64/ld-2.28.so
7f3c00603000-7f3c00605000 rw-p 0002f000 08:01 235980 /usr/lib64/ld-2.28.so
7ffdc053f000-7ffdc0560000 rw-p 00000000 00:00 0 [stack] #栈
7ffdc0595000-7ffdc0599000 r--p 00000000 00:00 0 [vvar]
7ffdc0599000-7ffdc059b000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

对应地,我们可以找到每个变量在虚拟内存中的地址范围。其中动态链接库是程序运行时动态加载的而其加载地址也是每次可能不一样的

以下区域被标记为代码段(文本段):

558d8d991000-558d8da99000 r-xp  这是/usr/bin/bash可执行文件的代码段,具有可执行和只读权限。

下面的区域被标记为数据段

558d8dc98000-558d8dc9c000 r--p 这是/usr/bin/bash可执行文件的只读数据段。
558d8dc9c000-558d8dca5000 rw-p 这是/usr/bin/bash可执行文件的可读写数据段。

共享库堆、栈的内存区域,见 maps 中的文件名。

崩溃程序定位

这里我们以如下的源码为例,演示如何定位崩溃的 Linux 程序。

源文件 segmentfault.c

/*
* segmentfault.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int add1(int num)
{
int ret = 0x00;
int *pTemp = NULL;

*pTemp = 0x01; /* 这将导致一个段错误,致使程序崩溃退出 */

ret = num + *pTemp;

return ret;
}

int add(int num)
{
int ret = 0x00;

ret = add1(num);

return ret;
}

源文件 dump.c :

/*
* dump.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> /* for signal */
#include <execinfo.h> /* for backtrace() */

#define BACKTRACE_SIZE 16

void dump(void)
{
int j, nptrs;
void *buffer[BACKTRACE_SIZE];
char **strings;

nptrs = backtrace(buffer, BACKTRACE_SIZE);

printf("backtrace() returned %d addresses\n", nptrs);

strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}

for (j = 0; j < nptrs; j++)
printf(" [%02d] %s\n", j, strings[j]);

free(strings);
}

void signal_handler(int signo)
{

#if 1
char buff[64] = {0x00};

sprintf(buff,"cat /proc/%d/maps", getpid());

system((const char*) buff);
#endif

printf("\n=========>>>catch signal %d <<<=========\n", signo);

printf("Dump stack start...\n");
dump();
printf("Dump stack end...\n");

signal(signo, SIG_DFL); /* 恢复信号默认处理 */
raise(signo); /* 重新发送信号 */
}

源文件 backtrace.c

/*
* backtrace.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> /* for signal */
#include <execinfo.h> /* for backtrace() */

extern void dump(void);
extern void signal_handler(int signo);
extern int add(int num);

int main(int argc, char *argv[])
{
int sum = 0x00;

signal(SIGSEGV, signal_handler); /* 为SIGSEGV信号安装新的处理函数 */

sum = add(sum);

printf(" sum = %d \n", sum);

return 0x00;
}

Makefile文件:

CC=gcc
CFLAGS += -g -I. -rdynamic  # 编译时加入-rdynamic可以防止strip将符号信息优化掉
LDFLAGS += -rdynamic        # 链接时只有加上-rdynamic才能将所有非静态函数加到动态符号表中,否在backtrace无法堆栈回溯
.PHONY: all

#要放第一行,makefile默认执行第一个target
all:segmentfault
##############################################
segmentfault.o:
        $(CC) $(CFLAGS) -c segmentfault.c -o segmentfault.o

dump.o:
        $(CC) $(CFLAGS) -c dump.c -o dump.o

backtrace.o:
        $(CC) $(CFLAGS) -c backtrace.c -o backtrace.o

segmentfault:segmentfault.o dump.o backtrace.o
        $(CC) $(LDFLAGS) segmentfault.o dump.o backtrace.o -o segmentfault

##############################################
cleantmp:
        rm -rf *.o

clean:
        rm -rf segmentfault.o segmentfault dump.o  backtrace.o

在程序崩溃时,系统会发送信号,在注册的信号处理函数中,将进程的maps文件保存下来,同时记录此时的函数调用链,利用这些信息就可以进行故障定位。

Linux二进制
学习并分享Linux的相关技术,网络、内核、驱动、容器等。
 最新文章