linux編程入門(九)-程序崩潰之後的排錯及定位

當我們寫程序時候難免會因爲各種問題崩掉,如果是開發階段,我們可以開gdb跟蹤調試,但如果到了線上,就不能用gdb了,這時候我們可以把崩潰時候的調用棧信息打印出來,然後定位到具體崩潰的代碼位置.
想要定位到具體的行號,需要在編譯的時候加入-g參數,表示編譯時候加入調試信息,調試信息裏有相關的信息可以使地址轉爲行號.
下面介紹幾個可以定位到崩潰位置的方法:

使用core文件

core文件其實是程序崩潰後的內存數據,也叫core dump或者dump文件,當得到core文件後就可以用gdb打開core文件,就能定位崩潰的位置了.

接下來我們先準備好一段測試代碼,後面就用這段代碼搞點事情
代碼裏我們故意除以0,使程序崩潰

 

float div(int a, int b){
    float c = a/b;
    return c;
}

int main(int argc, char** argv){
    (void)argc;
    (void)argv;

    int a = 10;
    int b = 0;
    float c = div(a, b);
    printf("div: %d/%d=%.2f\n", a, b, c);

    return 0;
}

假如我們編譯好的程序叫main,執行後,會顯示Floating point exception,也就是除0錯誤了

 

bash$ ./main
Floating point exception (core dumped)

這時候看一下當前目錄有沒有生成core文件,默認應該是沒有.

 

bash$ ls
main  main.cpp  main.o  Makefile

當沒有產生core文件的時候,就是開關沒開,需要先打開開關
先查看一下ulimit -c的值

 

bash$ ulimit -c
0

上面顯示結果爲0,表示禁止生成core文件,下面我們設置爲不限制core大小

 

bash$ ulimit -c unlimited

# 再用ulimit -c查看一下
bash$ ulimit -c
unlimited

然後再運行下程序看看,就生成core文件了

 

bash$ ./main
Floating point exception (core dumped)
bash$ ls
core  main  main.cpp  main.o  Makefile

接着用gdb打開core文件,打開就是崩潰的位置

 

bash$ gdb --core=./core main
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...done.
[New LWP 15220]
Core was generated by `./main'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0  0x0000559cc0dbe143 in div (a=10, b=0) at main.cpp:5
5               float c = a/b;
(gdb)

可以bt看一下調用棧

 

(gdb) bt
#0  0x0000559cc0dbe143 in div (a=10, b=0) at main.cpp:5
#1  0x0000559cc0dbe182 in main (argc=1, argv=0x7ffd65f14f88) at main.cpp:15

上面說的ulimit -c unlimited這種方法在重啓機器後就會失效,想永久打開生成core,需要修改些配置,這裏大家可以下去自己研究一下,後面我們就說一下,假如沒有core文件的時候,我們該怎麼辦.

在代碼崩潰的時候把調用棧打印出來

core文件其實是靠不住的,因爲core文件是程序的內存數據,當程序特別大的時候core文件就特別大,如果機器磁盤已經快滿了,再想生成core文件就生不成了,這時候我們可以自己在程序裏把程序崩潰時候的關鍵信息打印出來,就不需要太依賴core文件了
好,自己動手,豐衣足食.

想要打印調用棧,我們想到的就是bt這個詞,也就是backtrace,也可以上網搜一下程序打印調用棧,得出來的結果就是用backtrace這個函數可以打印調用棧,我們可以man backtrace看一下這個函數,結果man裏就有一段測試backtrace的代碼,太爽了,man手冊就是好用,裏面有很多測試代碼.
比如想學網絡編程,就man一下socket,打開後,向下翻手冊,如果沒有看到測試代碼就注意最後面有個SEE ALSO,這裏會列出和socket相關的函數,挨個man這些函數,總有你想看到的測試代碼,到時候拿來練習就可以了.

接下來我們就直接上碼了,對着程序走一遍就會了

 

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <assert.h>

void myfunc3(void){
    int j, nptrs;
#define SIZE 100
    void *buffer[100];
    char **strings;

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

    /* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
       would produce similar output to the following: */

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

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

    free(strings);
}

void dump_backtrace(int signum){
    printf("dump_backtrace on signal:%d\n", signum);
    signal(signum,SIG_DFL);
    myfunc3();
}

/* "static" means don't export the symbol... */
static void myfunc2(void){
    myfunc3();
}

void myfunc(int ncalls){
    if (ncalls > 1)
        myfunc(ncalls - 1);
    else
        myfunc2();
}

void test_sig_segv(){
    //這個就是段錯誤了
    int* p = NULL;
    printf("*p = %d\n", *p);
}

void test_sig_abort(){
    abort();
}

void test_sig_abort_assert(){
    assert( 0 );
}

void test_sig_abort_free(){
    int* p = (int*)malloc(sizeof(int));
    *p = 1;
    printf("test_sig_abort_free: 0x%p %d\n", p, *p );
    free(p);
    printf("free once\n");
    free(p);
    printf("free twice\n");
}

void test_sig_fpe(){
    int a = 10;
    int b = 0;
    int c = a / b;
    printf("%d / %d = %d\n", a, b, c);
}

int main(int argc, char *argv[]) {
    (void)argc;
    (void)argv;

    int pid = getpid();
    printf("pid %d\n", pid);
#if 0
    //這裏是man手冊裏backtrace的測試代碼,直接打印調用棧
    if (argc != 2) {
        fprintf(stderr, "%s num-calls\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    myfunc(atoi(argv[1]));
    exit(EXIT_SUCCESS);
#else
    //這裏是先註冊信號,當程序運行出錯的時候,
    //捕捉到信號後打印調用棧,
    //實際使用中也是這種辦法
    signal(SIGSEGV, dump_backtrace); //segmentation violation
    signal(SIGABRT, dump_backtrace); //abort program (formerly SIGIOT)
    signal(SIGFPE, dump_backtrace); //floating-point exception

    test_sig_segv();
    //test_sig_abort();
    //test_sig_abort_assert();
    //test_sig_abort_free();
    //test_sig_fpe();

#endif
    return 0;
}

這裏需要把Makefile也貼一下,注意下面加上了鏈接時候用的參數-rdynamic,這個參數的作用是讓鏈接器把所有符號添加到動態符號表中,聽起來比較抽象,後面咱們通過例子來看一下效果就明白了.

 

LINK    = @echo linking $@ && g++
GCC     = @echo compiling $@ && g++
GC      = @echo compiling $@ && gcc
AR              = @echo generating static library $@ && ar crv
FLAGS   = -g -DDEBUG -W -Wall -fPIC
#FLAGS   = -DDEBUG -W -Wall -fPIC
#FLAGS   = -DNDEBUG -W -Wall -fPIC
#FLAGS   = -g -DNDEBUG -W -Wall -fPIC
GCCFLAGS =
DEFINES =
HEADER  = -I./
LIBS    =
LINKFLAGS =

#注意這裏不加-rdynamic的話,backtrace顯示的只有地址,不能顯示地址對應的符號
LINKFLAGS += -rdynamic

#LIBS    += -lrt
#LIBS    += -pthread

OBJECT := main.o \

BIN_PATH = ./

TARGET = main

$(TARGET) : $(OBJECT)
        $(LINK) $(FLAGS) $(LINKFLAGS) -o $@ $^ $(LIBS)

.cpp.o:
        $(GCC) -c $(HEADER) $(FLAGS) $(GCCFLAGS) -fpermissive -o $@ $<

.c.o:
        $(GC) -c $(HEADER) $(FLAGS) -fpermissive -o $@ $<

install: $(TARGET)
        cp $(TARGET) $(BIN_PATH)

clean:
        rm -rf $(TARGET) *.o *.so *.a

上面的大多數代碼都是從man backtrace裏扒出來的,但是man手冊裏的代碼是調用程序就直接打印調用棧了,和我們的需求不符,我們希望在程序崩潰的時候再打印,那就得改一下,程序崩潰的時候會觸發一些相應的信號,我們只要在程序裏捕捉到這些信號,然後在收到信號的時候再打印調用棧就可以了.
下面我們看看捕捉信號這幾行代碼

 

# 這部分代碼里加了註釋,下面的signal意思是註冊一個信號,當發生該信號時,回調後面的函數,
# 下面是註冊了三個信號, 
# SIGSEGV是段錯誤,這錯誤就是指針相關的問題一般會引起該錯誤,比如取一個空指針的值
# SIGABRT是當程序用了abort()或者assert之類的函數時候會觸發
# SIGFPE也就是除0時候發生的了
    //這裏是先註冊信號,當程序運行出錯的時候,
    //捕捉到信號後打印調用棧,
    //實際使用中也是這種辦法
    signal(SIGSEGV, dump_backtrace); //segmentation violation
    signal(SIGABRT, dump_backtrace); //abort program (formerly SIGIOT)
    signal(SIGFPE, dump_backtrace); //floating-point exception

然後我們再看一下dump_backtrace函數

 

#當程序崩潰後,我們把收到的信號值打了出來,
#然後用signal(signum,SIG_DFL)讓程序默認處理該信號
#接着用myfunc3把調用棧打印了出來,實際項目中我們可以把這些信息寫到日誌裏
void dump_backtrace(int signum){
    printf("dump_backtrace on signal:%d\n", signum);
    signal(signum,SIG_DFL);
    myfunc3();
}

myfunc3裏具體輸出調用棧我們就不說了,都是抄來的東西,有興趣的同學可以自己研究一下.
下面我們看一下程序運行效果,在編譯的時候,咱們先把Makefile裏的
LINKFLAGS += -rdynamic 這行註釋掉

 

bash$ ./main
pid 15562
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1223) [0x555d55255223]
./main(+0x1321) [0x555d55255321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f6585563100]
./main(+0x136c) [0x555d5525536c]
./main(+0x14ce) [0x555d552554ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f658554609b]
./main(+0x113a) [0x555d5525513a]
Segmentation fault (core dumped)

代碼裏我們用的是引起段錯誤的代碼,所以信號爲11,也就是SIGSEGV,下面是各信號的編號和含義

 

    查看signal man手冊,可以看到下面的signal定義。
     1     SIGHUP       terminate process    terminal line hangup
     2     SIGINT       terminate process    interrupt program
     3     SIGQUIT      create core image    quit program
     4     SIGILL       create core image    illegal instruction
     5     SIGTRAP      create core image    trace trap
     6     SIGABRT      create core image    abort program (formerly SIGIOT)
     7     SIGEMT       create core image    emulate instruction executed
     8     SIGFPE       create core image    floating-point exception
     9     SIGKILL      terminate process    kill program
     10    SIGBUS       create core image    bus error
     11    SIGSEGV      create core image    segmentation violation
     12    SIGSYS       create core image    non-existent system call invoked
     13    SIGPIPE      terminate process    write on a pipe with no reader
     14    SIGALRM      terminate process    real-time timer expired
     15    SIGTERM      terminate process    software termination signal
     16    SIGURG       discard signal       urgent condition present on socket
     17    SIGSTOP      stop process         stop (cannot be caught or ignored)
     18    SIGTSTP      stop process         stop signal generated from keyboard
     19    SIGCONT      discard signal       continue after stop
     20    SIGCHLD      discard signal       child status has changed
     21    SIGTTIN      stop process         background read attempted from control terminal
     22    SIGTTOU      stop process         background write attempted to control terminal
     23    SIGIO        discard signal       I/O is possible on a descriptor (see fcntl(2))
     24    SIGXCPU      terminate process    cpu time limit exceeded (see setrlimit(2))
     25    SIGXFSZ      terminate process    file size limit exceeded (see setrlimit(2))
     26    SIGVTALRM    terminate process    virtual time alarm (see setitimer(2))
     27    SIGPROF      terminate process    profiling timer alarm (see setitimer(2))
     28    SIGWINCH     discard signal       Window size change
     29    SIGINFO      discard signal       status request from keyboard
     30    SIGUSR1      terminate process    User defined signal 1
     31    SIGUSR2      terminate process    User defined signal 2

可以看到上面輸出了程序的pid爲15562,接着有./main(+0x1223)的字樣,有好幾行,每一行就是一層調用棧,最上面的是最後調用的函數,其實就是myfunc3()打印調用棧的函數,崩潰的地方在往下幾行裏面,這裏我們僅介紹一下從地址得到函數的方法. 這裏面的+0x1223就是代碼的地址,我們需要用另一個工具把該地址翻譯爲代碼行號.

 

./main(+0x1223) [0x555d55255223]

用addr2line把地址轉爲函數名和行號,注意只有編譯時候加了-g參數的時候纔會行到行號

 

# addr2line的參數說明 
# -e表示指明程序名爲main
# -s表示輸入代碼名的時候不要帶路徑
# -f表示顯示函數名
# -C表示顯示demangle後的函數名,demangle是針對mangle來說的,程序在編譯後函數名都被轉爲函數符號了,該符號不便於我們識別,所以需要demangle一下
bash$ addr2line -e main 0x1223 -sfC
myfunc3()
main.cpp:14

# 下面是隻顯示代碼文件和行號的參數
bash$ addr2line -e main 0x1223 -s
main.cpp:14

# 下面是沒有經過demangle的函數名
bash$ addr2line -e main 0x1223 -sf
_Z7myfunc3v
main.cpp:14

也可以用gdb查看地址所在的函數名,但是這樣查不到行號,沒有addr2line好用

 

# 注意,gdb只能加載main,不要run,一run就無法解析地址了
Reading symbols from ./main...done.
(gdb) i symbol 0x1223
myfunc3() + 46 in section .text
(gdb)

小知識
可以用nm查看程序裏的符號,比如我們nm一下main函數

 

bash$ nm main
                 U abort@@GLIBC_2.2.5
                 U __assert_fail@@GLIBC_2.2.5
                 U backtrace@@GLIBC_2.2.5
                 U backtrace_symbols@@GLIBC_2.2.5
0000000000004010 B __bss_start
0000000000004010 b completed.7931
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004000 W data_start
0000000000001140 t deregister_tm_clones
00000000000011b0 t __do_global_dtors_aux
0000000000003d60 t __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003d68 d _DYNAMIC
0000000000004010 D _edata
0000000000004018 B _end
                 U exit@@GLIBC_2.2.5
0000000000001544 T _fini
00000000000011f0 t frame_dummy
0000000000003d58 t __frame_dummy_init_array_entry
0000000000002384 r __FRAME_END__
                 U free@@GLIBC_2.2.5
                 U getpid@@GLIBC_2.2.5
0000000000003f58 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000000020e0 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003d60 t __init_array_end
0000000000003d58 t __init_array_start
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000001540 T __libc_csu_fini
00000000000014e0 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000001460 T main
                 U malloc@@GLIBC_2.2.5
                 U perror@@GLIBC_2.2.5
                 U printf@@GLIBC_2.2.5
                 U puts@@GLIBC_2.2.5
0000000000001170 t register_tm_clones
                 U signal@@GLIBC_2.2.5
                 U __stack_chk_fail@@GLIBC_2.4
0000000000001110 T _start
0000000000004010 D __TMC_END__
0000000000001421 T _Z12test_sig_fpev
0000000000001358 T _Z13test_sig_segvv
00000000000012ec T _Z14dump_backtracei
0000000000001384 T _Z14test_sig_abortv
00000000000013b0 T _Z19test_sig_abort_freev
000000000000138d T _Z21test_sig_abort_assertv
0000000000001330 T _Z6myfunci
0000000000001330 t _Z6myfunci.localalias.0
00000000000011f5 T _Z7myfunc3v
0000000000001324 t _ZL7myfunc2v
00000000000020c0 r _ZZ21test_sig_abort_assertvE19__PRETTY_FUNCTION__

可以看到裏面函數名都是mangle後的符號,可以用c++filt demangle一下
bash$ c++filt _Z6myfunci
myfunc(int)

下面我們把Makefile裏的
LINKFLAGS += -rdynamic
再解屏,看看效果

 

bash$ ./main
pid 15697
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(_Z7myfunc3v+0x2e) [0x55e95c4a0223]
./main(_Z14dump_backtracei+0x35) [0x55e95c4a0321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f46e4fff100]
./main(_Z13test_sig_segvv+0x14) [0x55e95c4a036c]
./main(main+0x6e) [0x55e95c4a04ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f46e4fe209b]
./main(_start+0x2a) [0x55e95c4a013a]
Segmentation fault (core dumped)

可以看到生成的不是地址了,而是./main(函數符號+偏移地址),那這樣我們怎麼求出崩潰地址呢?
這裏就需要用到上面小知識裏說到的nm了,我們可以從nm裏查到相應符號的地址,再加上偏移地址,就可以得到崩潰時候的地址了.

 

# 假如我們要求下面_Z7myfunc3v+0x2e的地址
./main(_Z7myfunc3v+0x2e) [0x55e95c4a0223]

bash$ nm main | grep _Z7myfunc3v
00000000000011f5 T _Z7myfunc3v

#查到地址是00000000000011f5, 然後再用0x11f5 + 0x2e就可以了
#先把16進制轉爲10進制
bash$ printf "%d %d\n" 0x11f5 0x2e
4597 46

#再expr求一下
bash$ expr 4597 + 46
4643

#再轉爲16進制
bash$ printf "%x\n" 4643
1223

#再addr2line看一下就可以得到行號了
bash$ addr2line -e main 0x1223 -sfC
myfunc3()
main.cpp:14

接着我們再做幾個測試,之前我們用的是debug, -g編譯參數,下面我們分別做release和不帶-g的測試
看看崩潰後是否還能依據地址找到代碼位置

 

# 先測試-DDEBUG 不帶 -g
# Makefile裏用這句
FLAGS   = -DDEBUG -W -Wall -fPIC

# 爲了能直接得到地址,咱們把-rdynamic去掉,這樣就不用再求地址了
#LINKFLAGS += -rdynamic

運行一下程序

 

bash$ ./main
pid 19316
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1223) [0x560986db5223]
./main(+0x1321) [0x560986db5321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7fc880e69100]
./main(+0x136c) [0x560986db536c]
./main(+0x14ce) [0x560986db54ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fc880e4c09b]
./main(+0x113a) [0x560986db513a]
Segmentation fault (core dumped)

addr2line看一下,結論,debug模式下,去掉-g看不到行號了,因爲沒有調試信息了

 

bash$ addr2line -e main 0x1223 -sfC
myfunc3()
??:?

再來用release, -g測試一下

 

#Makefile裏用這句
FLAGS   = -g -DNDEBUG -W -Wall -fPIC

運行一下,讓程序崩一個,注意,代碼沒變, release和debug崩的地址變了,debug是0x1223,release是0x1213

 

bash$ ./main
pid 19340
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1213) [0x5602bc813213]
./main(+0x1311) [0x5602bc813311]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7fd0aedd5100]
./main(+0x135c) [0x5602bc81335c]
./main(+0x14a2) [0x5602bc8134a2]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fd0aedb809b]
./main(+0x112a) [0x5602bc81312a]
Segmentation fault (core dumped)

再來addr2line看一下,結論: release下,帶-g可以看到行號和函數符號

 

bash$ addr2line -e main 0x1213 -sfC
myfunc3()
main.cpp:14

再測試最後一種情況,release不帶-g

 

#Makefile裏用這句,其他的FLAGS屏掉
FLAGS   = -DNDEBUG -W -Wall -fPIC

編完,跑一下程序

 

bash$ ./main
pid 19367
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1213) [0x55801b953213]
./main(+0x1311) [0x55801b953311]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f823c12b100]
./main(+0x135c) [0x55801b95335c]
./main(+0x14a2) [0x55801b9534a2]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f823c10e09b]
./main(+0x112a) [0x55801b95312a]
Segmentation fault (core dumped)

addr2line看一下,結論,release不帶-g看不到行號,依然能看到函數符號

 

bash$ addr2line -e main 0x1213 -sfC
myfunc3()
??:?

總結一下:

  • 不管是release還是debug,都能看到函數符號
  • 帶-g能看到行號
  • 不帶-g看不到行號
    所以線上時候我們一般會打個release的程序,但是帶-g,便於定位

使用dmesg求出崩潰所在的位置

最後這個辦法是linux自帶的,當程序崩潰後,系統會記個log,當我們沒有core,也沒有自己打印backtrace,或者由於一些原因,core或log被刪掉了,總之程序崩了之後什麼線索都沒有的時候,就可以用dmesg定位了.

我們這次用debug, -g先編好程序,跑一下

 

bash$ ./main
pid 19429
dump_backtrace on signal:11
backtrace() returned 7 addresses
./main(+0x1223) [0x55fa146b3223]
./main(+0x1321) [0x55fa146b3321]
/lib/x86_64-linux-gnu/libc.so.6(+0x41100) [0x7f5faa0ee100]
./main(+0x136c) [0x55fa146b336c]
./main(+0x14ce) [0x55fa146b34ce]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f5faa0d109b]
./main(+0x113a) [0x55fa146b313a]
Segmentation fault (core dumped)

再用dmesg查一下崩潰log,因爲dmesg會輸出一大堆東西,我們過濾一下,只看帶main的,因爲我們程序名叫main

 

bash$ dmesg | grep main
... # 前面的略
[159708.390262] traps: main[12287] trap divide error ip:55644504443b sp:7fffc2a9c230 error:0 in main[556445044000+1000]
... # 中間的略
[239814.618107] main[19429]: segfault at 0 ip 000055fa146b336c sp 00007ffd4b2b4180 error 4 in main[55fa146b3000+1000]

上面我截了兩行內容,dmesg最下面的是最後一次發生崩潰的記錄,說一下輸出的內容

 

[系統開啓的秒數] 程序名[pid]: 錯誤原因 ip 指令地址 sp 棧頂地址 錯誤號 main[基址+大小]
[239814.618107] main[19429]: segfault at 0 ip 000055fa146b336c sp 00007ffd4b2b4180 error 4 in main[55fa146b3000+1000]

下面的錯誤原因分別有trap divide error,表示除0的錯,另一個segfault at 0是空指針引起的段錯誤, 後面的error號也不一樣.
拿到這個log的時候,我們看到最後一條記錄main[19429],pid正好與最後一次我們運行程序時候打印的pid相同,表示是同一次崩潰了.

下面我們開始算術,求出崩潰地址.
我們需要關注的是 ip 000055fa146b336c 和 main[55fa146b3000+1000] 這兩列,ip指的是指令在崩潰的時候指向的地址, main後面是程序的基址,所以我們可以想到ip - bp 就是偏移地址(bp是基址的意思),也就是程序崩潰時候的地址了,下面我們驗證一下

 

# 先把ip, bp轉爲10進制,方便expr運算
bash$ printf "%d %d %d\n" 0x000055fa146b336c 0x55fa146b3000 0x1000
94532572754796 94532572753920 4096

# 再epxr算一下 addr = ip - bp
bash$ expr 94532572754796 - 94532572753920 + 4096
4972

# 再把4972轉爲16進制
printf "%x\n" 4972
136c

下面我們拿addr2line查一下這個地址的行號,定位了

 

bash$ addr2line -e main 0x136c -sfC
test_sig_segv()
main.cpp:54

下面就是引起崩潰的位置了,確實是54行

 

    51  void test_sig_segv(){
    52          //這個就是段錯誤了
    53          int* p = NULL;
    54          printf("*p = %d\n", *p);
    55  }



作者:程序大飛
鏈接:https://www.jianshu.com/p/a0224216b7cb
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章