Reading Coredump

โดยปกติการอ่าน coredump เป็นพื้นฐานที่ง่ายที่สุดครับ เพราะเป็นการอ่านสิ่งที่ตายแล้ว ไม่ต้องขยับอะไรทั้งนั้น เราก็จะมาเริ่มใช้ GDB ที่อันนี้ก่อน เพื่อเข้าใจว่าอ่านค่าอะไรยังไงบ้าง

ถ้าเริ่มทำตามผมตั้งแต่ preparation จะได้ coredump มาแล้ว ก็เริ่มจากเอา GDB เข้าไปอ่าน core ให้ใช้คำสั่งตามนี้ครับ gdb ./<executable> <core_of_executable>

$ gdb ./example.o core
Reading symbols from ./example.o...done.
[New LWP 17137]
Core was generated by `./example.o'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000055bc13b7b9a0 in getInputAndRun () at example.c:28
28        *result[1] = toupper(result[1][0]);
(gdb)

ดู stack ของโปรแกรม ปัจจุบันด้วย bt หรือ backtrace

(gdb) bt
#0  0x000055bc13b7b9a0 in getInputAndRun () at example.c:28
#1  0x000055bc13b7ba1f in main () at example.c:37
(gdb) f 1
#1  0x000055bc13b7ba1f in main () at example.c:37
37        if (getInputAndRun() == -1) break;
(gdb) f 0
#0  0x000055bc13b7b9a0 in getInputAndRun () at example.c:28
28        *result[1] = toupper(result[1][0]);

ดูง่ายนะครับ แปลว่า stack ของ program เราปัจจุบันรัน 2 function อยู่คือ main() และ getInputRun() เราสามารถกระโดดไปมาด้วยด้วยคำสั่ง frame <stack_no> หรือ f <stack_no>

ต่อมาดู source code ด้วย list

(gdb) f 0
#0  0x000055bc13b7b9a0 in getInputAndRun () at example.c:28
28        *result[1] = toupper(result[1][0]);
(gdb) list
23      fgets(input, 100, stdin);
24      if (strcmp(input, "exit") == 0) return -1;
25      else {
26        char** result = split(input);
27        *result[0] = toupper(result[0][0]);
28        *result[1] = toupper(result[1][0]);
29        printf("First str: %s\n", result[0]);
30        printf("Second str: %s\n", result[1]);
31        return 0;
32      }
(gdb) f 1
#1  0x000055bc13b7ba1f in main () at example.c:37
37        if (getInputAndRun() == -1) break;
(gdb) list
32      }
33    }
34
35    int main() {
36      while (1) {
37        if (getInputAndRun() == -1) break;
38      }
39      return 0;
40    }

หรือสั่ง list แบบมี parameter เพื่ออ่านบรรทัดที่ระบุ หรือ function ที่ระบุก็ได้ครับ

(gdb) list 1,2
1    #include <stdio.h>
2    #include <stdlib.h>
(gdb) list main
30        printf("Second str: %s\n", result[1]);
31        return 0;
32      }
33    }
34
35    int main() {
36      while (1) {
37        if (getInputAndRun() == -1) break;
38      }
39      return 0;

นอกจาก list ที่เป็น source code ภาษา c แล้ว เรายังสามารถอ่านเป็นภาษา assembly ได้ด้วย อันนี้แล้วแต่ที่ถนัดเลยครับ ถ้า compile พร้อม debug flag (-g) แทบไม่ต้องลง assembly เลย แต่หลายครั้งการลงไปดู assembly ก็ทำให้เราเห็นรายละเอียดอะไรเยอะกว่าเช่นกัน

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000555555554a11 <+0>:    push   %rbp
   0x0000555555554a12 <+1>:    mov    %rsp,%rbp
=> 0x0000555555554a15 <+4>:    mov    $0x0,%eax
   0x0000555555554a1a <+9>:    callq  0x555555554905 <getInputAndRun>
   0x0000555555554a1f <+14>:    cmp    $0xffffffff,%eax
   0x0000555555554a22 <+17>:    je     0x555555554a26 <main+21>
   0x0000555555554a24 <+19>:    jmp    0x555555554a15 <main+4>
   0x0000555555554a26 <+21>:    nop
   0x0000555555554a27 <+22>:    mov    $0x0,%eax
   0x0000555555554a2c <+27>:    pop    %rbp
   0x0000555555554a2d <+28>:    retq   
End of assembler dump.

เนื่องจาก stack ของ main ไม่มีอะไรให้เราดู เราจะดู stack getInputAndRun() กันนะครับ เริ่มจากอ่านตัวแปรต่างๆ ด้วย p <variable_name> หรือ x/<type> <variable_or_address> อาจจะงงๆ มาดูตัวอย่างเลยดีกว่า

(gdb) f 0
#0  0x000055bc13b7b9a0 in getInputAndRun () at example.c:28
28        *result[1] = toupper(result[1][0]);
(gdb) p result[0]
$2 = 0x7ffc4f8c7060 "Helloworld\n"
(gdb) p result[1]
$3 = 0x0
(gdb) x/s result[0]
0x7ffc4f8c7060:    "Helloworld\n"
(gdb) x/s result[1]
0x0:    <error: Cannot access memory at address 0x0>
(gdb) x/16xb result[0]
0x7ffc4f8c7060:    0x48    0x65    0x6c    0x6c    0x6f    0x77    0x6f    0x72
0x7ffc4f8c7068:    0x6c    0x64    0x0a    0x00    0x00    0x00    0x00    0x00
(gdb) x/s 0x7ffc4f8c7060
0x7ffc4f8c7060:    "Helloworld\n"

ตัว p คือ print ค่าออกมาครับ ซึ่งมันจะพยายามเดาประเภทของค่านั้นๆแล้ว print ให้เราอ่านง่าย แต่หลายครั้งมันก็พยายามฉลาดเกินไป เช่น p result[1] มันเห็นว่าค่าเป็น 0 เลยน่าจะเป็น integer ก็เลย print 0x0 มาให้ แต่เรารู้ว่าจริงๆมันเป็น string พอสั่ง x/s result[1] จะเห็นว่าได้ error กลับมาครับ

อันนี้ก็จบละ พื้นฐาน GDB ง่ายที่สุด คือการ print ตัวแปรออกมาดูว่าเกิดอะไรขึ้น ซึ่งถ้าเรา compile -g ไว้ก็จะเรียกตัวแปรตรงๆได้เลย แต่ถ้าเราไม่ได้ใส่ -g มา แต่มี coredump อยู่ เราก็ยังคงเรียกดูค่าใน address ก็ได้เหมือนกัน (แต่ต้องอ่าน assembly เป็นถึงจะรู้ว่า address ไหนคืออะไร)

อย่างข้อนี้เราก็เห็นแล้วว่ามันตายที่ example.c:28 ซึ่งก็ตายเพราะ result[1] มันมีค่าเป็น 0 นั่นเองทำให้ไม่สามารถ access เพื่อสั่ง toupper() ได้ เราก็ไปดูต่อว่าทำไมมันมีค่าเป็น 0 ต่อไป

ก็หวังว่าผู้อ่านรู้ถึงตรงนี้แล้ว จะไม่ไปนั่ง debug ด้วยการใส่ printf f-word เข้าไปทีละบรรทัด แล้ว compile + run ใหม่อีกนะครับ ใช้วิธีนี้ดีกว่า สะดวกเร็ว และไม่เผลอทิ้งขยะไว้ในโค้ดด้วย

Last updated

Was this helpful?