Memory Debugging ด้วย Valgrind

เช้าวันนึงขณะที่ผมกำลังทำงาน ก็พบขึ้นมาว่าโปรเจคที่ตัวเองทำอยู่ จู่ๆมันก็พังขึ้นมาตอนรันซะงั้น (ได้ไง T_T) มองผ่านๆก็ยังหา Error ไม่เจอซักที รู้แต่มันฟ้องว่ามีปัญหาที่การจัดการ Memory (เป็นที่ฟังก์ชัน realloc กับ free) ผมจึงเริ่มไปค้นว่ามีวิธีไหนบ้างมั้ย ที่จะช่วยเช็คว่าปัญหาอยู่ตรงไหนกันแน่ เพราะตอนนั้นหายังไงก็หาไม่เจอจริงๆ (เป็นโค้ดที่คนอื่นเขียน ผมอ่านแล้วก็มึนๆเล็กน้อย) ลองหาตามบอร์ดก็เลยไปเจอเจ้า tool ตัวนึงที่ชื่อ Valgrind ซึ่งหลังจากลองใช้แล้วก็พบว่าปาฏิหาริย์มีจริง มันหาตำแหน่งของโค้ดที่จัดการกับ Memory ไม่ถูกต้องให้ได้ดีทีเดียว

Valgrind คืออะไร? Valgrind เป็นตัว Debugger ครับ แต่เน้นไปทางการตรวจสอบการทำงานที่ผิดพลาดของ Memory เป็นส่วนใหญ่ (แน่นอนว่าทำอย่างอื่นได้ด้วย แต่ดูเหมือนจุดเด่นของมันจะอยู่ที่การตรวจสอบ Memory มากกว่า) สามารถตรวจสอบได้ว่าเกิดปัญหาอะไรบ้างกับโปรแกรมที่เราเขี้ยนขึ้น เช่น Memory Leak, Invalid Write, Invalid Free และสามารถบอกเราได้ทันทีว่าปัญหามาจากโคดส่วนไหน วิธีติดตั้งก็ไม่ลำบากยากเย็นอะไร แค่ติดตั้ง package ที่ชื่อ valgrind ก็เป็นอันเสร็จสิ้นพิธี (ลืมบอกไปว่าเว็บไซต์หลักของ Valgrind คือ http://valgrind.org)

ก่อนอื่นโปรแกรมที่จะใช้ Valgrind ในการ Debug ได้ จะต้องคอมไพล์ด้วยการเพิ่มออพชัน -g เข้าไปก่อน (เช่น gcc -g myprog.c) จากนั้นก็รันโปรแกรมที่จะทดสอบด้วยคำสั่ง

valgrind --tool=memcheck --leak-check=full programname arg1 arg2 ...

--tool=memcheck หมายถึงให้ทำการตรวจสอบ Memory ว่ามีข้อผิดพลาดอยู่ตรงไหน ส่วน --leak-check=full คือบอกว่าให้ทำการตรวจสอบ Memory Leak ด้วย programname คือชื่อโปรแกรมที่เราจะรัน arg1 arg2 ... คือ Argument ของโปรแกรมของเราครับ ก็สั่งรันด้วยคำสั่งดังต่อไปนี้ก็เป็นอันเรียบร้อย Valgrind จะทำการเช็ค Memory ให้เรา แล้วก็แสดงผลออกมาให้ ซึ่งสิ่งที่เป็นเรื่องลำบากสำหรับการใช้ Valgrind ตอนแรกๆคือ output มันอ่านยาก ดูไม่รู้เรื่องเลย ผมเลยขอยกตัวอย่างข้อผิดพลาดที่พบบ่อย แล้วก็วิธีดู output กันซะหน่อย

เริ่มด้วยตัวอย่างของโปรแกรมที่ไม่ค่อยจะสมบูรณ์อันนี้ครับ

#include <stdlib.h>
#include <stdio.h> 

int main() {
  char *data = malloc(50);
  data[50] = 'K';
  printf("%s\n", data);
  return 0;
}

โปรแกรมนี้แค่มองผ่านๆก็จะรู้ทันทีว่ามีข้อผิดพลาดอยู่มากมาย อย่างแรกคือมัน Memory Leak แน่นอนครับ เพราะมันใช้ malloc แต่ไม่ได้เรียก free อย่างที่ 2 คือมันกำหนดค่าให้กับอาร์เรย์ตำแหน่งที่ 50 (ซึ่งมันมีแค่ 0 - 49) และอย่างสุดท้ายคือ มันเขียนข้อมูลบ้าอะไรออกไปก็ไม่รู้ เรายังไม่ทันได้กำหนดค่าให้แต่ก็ดันไปเรียก printf สมมติให้เราเซฟโปรแกรมนี้เป็นชื่อ myprog.c แล้วคอมไพล์ด้วยคำสั่ง gcc -g -o myprog myprog.c ก็จะได้โปรแกรมที่ชื่อ myprog ออกมา จากนั้นก็ทำการรันโดยใช้ Valgrind ด้วยคำสั่ง valgrind --tool=memcheck --leak-check=full ./myprog ก็จะได้ output ยาวเหยียดออกมาแบบนี้ครับ

==5743== Memcheck, a memory error detector
==5743== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==5743== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==5743== Command: ./myprog
==5743== 
==5743== Invalid write of size 1
==5743==    at 0x8048434: main (myprog.c:6)
==5743==  Address 0x418f05a is 0 bytes after a block of size 50 alloc'd
==5743==    at 0x4024F20: malloc (vg_replace_malloc.c:236)
==5743==    by 0x8048428: main (myprog.c:5)
==5743== 
==5743== Conditional jump or move depends on uninitialised value(s)
==5743==    at 0x402603B: strlen (mc_replace_strmem.c:282)
==5743==    by 0x40A2724: puts (ioputs.c:37)
==5743==    by 0x8048442: main (myprog.c:7)
==5743== 

==5743== 
==5743== HEAP SUMMARY:
==5743==     in use at exit: 50 bytes in 1 blocks
==5743==   total heap usage: 1 allocs, 0 frees, 50 bytes allocated
==5743== 
==5743== 50 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5743==    at 0x4024F20: malloc (vg_replace_malloc.c:236)
==5743==    by 0x8048428: main (myprog.c:5)
==5743== 
==5743== LEAK SUMMARY:
==5743==    definitely lost: 50 bytes in 1 blocks
==5743==    indirectly lost: 0 bytes in 0 blocks
==5743==      possibly lost: 0 bytes in 0 blocks
==5743==    still reachable: 0 bytes in 0 blocks
==5743==         suppressed: 0 bytes in 0 blocks
==5743== 
==5743== For counts of detected and suppressed errors, rerun with: -v
==5743== Use --track-origins=yes to see where uninitialised values come from
==5743== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 11 from 6)

เริ่มต้นมันก็จะเกริ่นว่ามันคือ tool ที่ชื่อว่า Memcheck บลาๆๆ ซึ่งเราก็แกล้งทำเป็นไม่สนใจมันไปก่อนครับ ข้ามมาที่บรรทัดที่ 6 เลย นั่นคือข้อความแรกที่มันฟ้องเราครับ ซึ่งมันบอกว่าสาเหตุคือ Invalid write of size 1 ซึ่งเจ้า Invalid write จะหมายถึงการเขียนข้อมูลลงไปในหน่วยความจำที่เราไม่ได้จองไว้ ถ้ามองบรรทัดต่อไปอีก (บรรทัดที่ 7) มันก็จะบอกต่ำแหน่งว่ามันคือโค้ดส่วนไหน  at 0x8048434: main (myprog.c:6) ซึ่งมันก็บอกเรียบร้อยเลยว่าบรรทัดที่ 6 แถมยังบอกสาเหตุอีกว่า Address 0x418f05a is 0 bytes after a block of size 50 alloc'd ซึ่งก็คือไปเขียนข้อมูลในหน่วยความจำถัดไปจากส่วนที่เราจองเอาไว้ 50 ช่อง พอเราย้อนกลับไปดูที่โปรแกรมก็จะเห็น ว่ามันมีปัญหาที่ data[50] = 'K' จริงๆนั่นแหละ (อย่าลืมว่าโปรแกรมจริงไม่ได้ตรวจสอบง่ายอย่างนี้นะครับ เลขที่เป็นค่าคงที่ทำให้เราเห็นว่ามันเกินออกมา แต่จริงๆแล้วเราอาจจะใช้ตัวแปรเป็น Subscript ก็ได้ ทำให้ตอนอ่านโปรแกรมไม่รู้ถึงข้อผิดพลาด กว่าจะรู้ว่ามันพังก็ตอนรันซะแล้ว)

Error ถัดไปฟ้องว่า Conditional jump or move depends on uninitialised value(s)  (บรรทัดที่ 12) ซึ่งหมายถึงการที่เราพยายามใช้หน่วยความจำในส่วนที่ยังไม่ได้กำหนดค่า (Uninitialize) นั่นเอง พอเรามองหาตำแหน่งมันก็จะบอกว่า mc_replace_strmem.c บรรทัดที่ 282 ซึ่งจริงๆแล้วมันไม่ใช่ มันพังที่ตัวที่เรียกมันต่างหาก ลองตามแต่ละบรรทัดไปเรื่อยๆจนเจอไฟล์ของเรา ก็จะเจอว่ามันผิดพลาดที่ myprog.c บรรที่ 7 ก็คือตอนที่เราสั่งพิมพ์ค่าของ data ออกไปนั่นเองครับ (เราไม่ควรที่จะพิมพ์ค่าออก เพราะยังไม่ได้กำหนดค่าเริ่มต้นให้มันก่อน)

ตั้งแต่บรรทัดที่ 19 เป็นต้นไป ก็จะบอกถึงการตรวจสอบ Memory Leak จะเจอข้อความว่า total heap usage: 1 allocs, 0 frees, 50 bytes allocated แปลว่าเราจอง (Allocate) ไปทั้งหมด 1 ครั้งในโปรแกรม แต่กลับไม่ยอมคืนให้ (Deallocate) เลย มันยังบอกต่อครับว่าที่จองไว้น่ะคือบรรทัด 5 ในโปรแกรม (by 0x8048428: main (myprog.c:5)) ส่วนที่เหลือว่าจะคืนตอนไหนก็เป็นหน้าที่คุณแล้วล่ะครับ มันคงตัดสินใจให้ไม่ได้

คราวหน้าจะมาต่อเรื่องข้อผิดพลาดอื่นๆที่เจอใน Valgrind ครับ คราวนี้ขอแค่นี้ก่อน รู้สึกว่ามันยาวเหลือเกิน = ="

Reference
1. http://cs.ecs.baylor.edu/~donahoo/tools/valgrind/messages.html
2. http://www.cprogramming.com/debugging/valgrind.html
3. http://valgrind.org/docs/manual/quick-start.html

Taxonomy upgrade extras: