Memory Debugging ด้วย Valgrind (ภาค 2 - ตีแผ่ Error แบบต่างๆ)

ต่อจากคราวก่อนนะครับ คราวนี้เราจะมาดูกันว่า Valgrind สามารถ ตรวจจับ Error แบบไหนได้บ้าง แล้วมันฟ้องออกมายังไงกัน

1. อ่านข้อมูลผิด (Invalid Read)

เป็นกรณีที่โปรแกรมของเราเข้าไปใช้งานส่วนของหน่วยความจำที่ไม่ได้จองเอาไว้ ตัวอย่างนี้มีในคราวที่แล้ว แต่จะขอยกมาให้ดูชัดๆอีกที สังเกตที่โปรแกรมด้านล่าง จะเข้าถึงข้อมูลในตำแหน่งที่ไม่ได้จองเอาไว้ (ช่องที่ 5 ของอาร์เรย์) พอตรวจสอบด้วย Valgrind ก็จะฟ้องว่า Invalid read of size ... (อย่าลืมว่าโปรแกรมจริง subscript อาจจะไม่ใช่ค่าคงที่แบบนี้ แต่เป็นตัวแปร ดังนั้นตำแหน่งของโค้ดที่ได้จาก Valgrind นั้นมีความสำคัญมากในการหาที่ผิด)

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

int main(){
	char *s = malloc(5);
	for(int i = 0; i < 5; i++)
		s[i] = 0;
	printf("%d\n", s[5]);
	free(s);
	return 0;
}
==17106== Invalid read of size 1
==17106==    at 0x8048455: main (example.c:8)
==17106==  Address 0x418f02d is 0 bytes after a block of size 5 alloc'd
==17106==    at 0x4024F20: malloc (vg_replace_malloc.c:236)
==17106==    by 0x8048428: main (example.c:5)

 

2. เขียนข้อมูลผิด (Invalid write)

คล้ายกับตัวอย่างที่แล้ว แต่กรณีจะเป็นการเข้าถึงหน่วยความจำแบบที่เราเข้าไปเขียนหรือแก้ไขค่า (ตัวอย่างก่อนอ่านเพียงอย่างเดียว) โปรแกรมตัวอย่างจะเริ่มดูยากขึ้นนิดนึง แต่ถ้าดูดีๆก็จะพบว่า ในส่วนของลูป for ทำการกำหนดค่าให้กับอาร์เรย์ช่องที่ 0 ถึง 5 ซึ่งมันควรจะถึงแค่ 4 เท่านั้น กรณีนี้นั่นเองที่เป็นการเขียนข้อมูลลงในตำแหน่งที่ไม่ได้จองไว้ และก็จะโดนฟ้องออกมาว่า Invalid write of size ... (สังเกตว่า Valgrind บอกเราทันทีว่าปัญหามาจากโค้ดในบรรทัดที่  7)

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

int main(){
	char *s = malloc(5);
	for(int i = 0; i < 6; i++)
		s[i] = 0;
	free(s);
	return 0;
}
==8368== Invalid write of size 1
==8368==    at 0x804843F: main (example.c:7)
==8368==  Address 0x418f02d is 0 bytes after a block of size 5 alloc'd
==8368==    at 0x4024F20: malloc (vg_replace_malloc.c:236)
==8368==    by 0x8048428: main (example.c:5)

 

3. ใช้ค่าของหน่วยความจำที่ยังไม่ได้กำหนดค่า (Use of uninitialized values)

เป็นข้อผิดพลาดที่เกิดจากการนำค่าในตำแหน่งที่เรายังไม่ได้มีการกำหนดค่ามาใช้ ตัวอย่างกคือประกาศตัวแปร a ขึ้นมา แต่ไม่ได้กำหนดค่าไว้ก่อน แต่นำมาปริ๊นทันที ซึ่งจริงๆแล้วในกรณีที่สมมติเราจองอาร์เรย์เอาไว้ 30 ช่อง และกำหนดค่าให้กบช่องที่ 0 ถึง 15 และ 17 ถึง 29 ไป แต่ลืมกำหนดค่าเริ่มต้นให้กับช่องที่ 16 แล้วนำอาร์เรย์ทั้งหมดไปใช้ Valgrind ก็ยังคงฟ้องเราอยู่ดี ว่ามีการใช้ค่าเริ่มต้นที่ยังไม่กำหนด

#include <stdio.h>

int main(){
	int a;
	printf("a = %d\n", a);
	return 0;
}
==17030== Conditional jump or move depends on uninitialised value(s)
==17030==    at 0x408424B: vfprintf (vfprintf.c:1613)
==17030==    by 0x408B69F: printf (printf.c:35)
==17030==    by 0x8048401: main (example.c:5)

 

4. ใช้ free() ไม่ถูกต้อง (Invalid free()) 

กรณีที่เราเรียกฟังก์ชันฟรีเพื่อคืนพื้นที่ไปแล้ว แต่เรายังเรียกฟรีซ้ำอีกครั้ง ก็ถือเป็นข้อผิดพลาดอย่างหนึ่งที่ทำให้โปรแกรมหยุดไปดื้อๆได้เหมือนกัน เช่นตัวอย่างข้างล่าง จองพื้นที่อาร์เรย์ในฟังก์ชัน main แต่มีการพื้นด้วย free ถึงสองครั้ง ทั้งในฟังกชัน main และ a (ในความเป็นจริง โปรแกรมหนึ่งอาจจะมีหลายไฟล์หลายฟังก์ชันมากกว่านี้ ทำให้เราไม่สามารถหาข้อผิดพลาดแบบนี้ได้ง่ายๆเลย)

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

void a(int *array, int size) {
	free(array);
}

int main(){
	int *array = malloc(20 * sizeof(int));
	a(array, 20);
	free(array);
	return 0;
}
==11464== Invalid free() / delete / delete[]
==11464==    at 0x4023B3A: free (vg_replace_malloc.c:366)
==11464==    by 0x804845F: main (example.c:11)
==11464==  Address 0x418e028 is 0 bytes inside a block of size 80 free'd
==11464==    at 0x4023B3A: free (vg_replace_malloc.c:366)
==11464==    by 0x8048424: a (example.c:5)
==11464==    by 0x8048453: main (example.c:10)

 

5. ส่งตัวแปรที่ยังไม่ได้กำหนดค่าไปให้ System Call (Use of uninitialised or unaddressable values in system calls)

ทุกครั้งที่มีการส่งพารามิเตอร์ไปยัง System Call นั้น Valgrind จะคอยตรวจสอบทุกครั้งว่ามีการใช้งานตัวแปรที่ยังไม่ได้กำหนดค่าหรือไม่ เช่นโปรแกรมข้างล่าง ที่สร้างอาร์เรย์ขึ้นมา โดยที่ยังไม่ทันกำหนดค่าก็ส่งไปยัง write เพื่อเขียนค่าในอาร์เรย์ออกทางหน้าจอ (1 คือ stdout) เมื่อรันด้วย Valgrind ก้จะได้ข้อความที่บอกว่า Syscall param write(buf) points to uninitialised byte(s) หมายถึงเราส่งตัวแปรที่ยังไม่ได้กำหนดค่าไปยัง System call ที่ชื่อ write (ถ้าลองรันโปรแกรมข้างล่างดูจะพบว่ามันจะแสดงค่ามั่วนิ่มออกมาทางหน้าจอ) ข้อควรระวังคือ แมจะเป็นกรณีการใช้งานตัวแปรที่ไม่ได้กำหนดค่าเหมือนกัน แต่การใช้งานผ่าน System call หรือไม่ ก็ทำให้ output ที่ได้จาก Valgrind ออกมาต่างกัน

#include <stdlib.h>
#include <unistd.h> 

int main(){
	char array[10];
	write(1, array, 10);
	return 0;
}
==8657== Syscall param write(buf) points to uninitialised byte(s)
==8657==    at 0x41031FE: __write_nocancel (syscall-template.S:82)
==8657==    by 0x405ABC5: (below main) (libc-start.c:226)
==8657==  Address 0xbeafa102 is on thread 1's stack

 

6. การ Copy ข้อมูลเป็นบล็อค ที่มีการซ้อนกันอยู่ (Overlapping source and destination blocks)

การใช้งานฟังก์ชันใน library ที่เป็นการ Copy ข้อมูลแบบเป็นช่วงๆ (หมายถึงค่าที่ติดกันหลายๆตัวนะครับ ไม่ใช่แพนด้า) ได้แก่ memcpy(), strcpy(), strncpy(), strcat(), strncat() นั้น Valgrindจะคอยเช็คว่าช่วงทั้งสองช่วงที่เรา Copy ไปนั้นมีพื้นที่ที่ซ้อนกัน (Overlapping) อยู่รึเปล่า ถ้ามีก็จะแจ้งเตือนขึ้นมา เช่นตัวอย่างด้านล่างนี้ ลองค่อยๆดูตามครับว่าถ้ารันแล้วจะเกิดอะไรขึ้น

#include <string.h>

int main(){
	char string[50] = "This is a book\n";
	strcpy(string, string + 2);
	return 0;
}
==9309== Source and destination overlap in strcpy(0xbebbb0ca, 0xbebbb0cf)
==9309==    at 0x4026170: strcpy (mc_replace_strmem.c:311)
==9309==    by 0x80484D9: main (example.c:5)

ตัวอย่างที่ 6 ที่เป็นการ Copy สตริงนั้น อาจจะไม่ผิดเสมอไป บางทีอาจเป็นความตั้งใจของคนเขียนเองให้ออกมาเป็นแบบนั้น ถ้าเป็นกรณีที่ว่าก็ไม่มีปัญหาครับ ถึงมันจะเตือนแต่ก็ไม่สนใจก็ได้ แต่ลองคิดกรณีที่เปลี่ยนจากคำสั่ง strcpy(string, string + 2) เป็น strcpy(string + 2, string) ดูครับว่ามันจะเกิดอะไรขึ้น

 

7. Memory Leak

อันนี้คงจะไม่ลงลึกมากครับ ลองกลับไปดูตอนที่ 1 น่าจะง่ายกว่า (เริ่มขี้เกียจเขียน) จริงๆแล้วไม่ใช่ว่าไม่มีอะไรจะเขียน แต่เรื่องนี้ถ้าจะเขียนคงยาวพอดู ก็ไม่แน่ว่าอาจจะมาเขียนเป็นตอนที่ 3 ของการใช้ Valgrind ไปเลยก็แล้วกัน

 

มี Error อะไรที่ Valgrind ไม่สามารถหาเจอมั้ย?

มีครับ ถึงมันจะดูสะดวกแล้วก็ทรงพลังก็จริง แต่ว่ายังมีกรณีที่ตรวจจับไม่เจอคือ กรณีของขนาดของ Static Array (Array ที่จองแบบ Static Allocation) เช่นตัวอย่างด้านล่าง Valgrind บอกได้แค่ว่า เป็นการอ่านค่าจากหน่วยความจำที่ยังไม่ได้กำหนดค่าเริ่มต้น แต่บอกไม่ได้ว่า ใช้อาร์เรย์ในตำแหน่งที่ไม่ถูกต้อง

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

int main(){
	int array[10];
	printf("%d\n", array[10]);
	return 0;
}

 

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

 

น่าจะพอเป็น Tutorial ฉบับย่อๆ สำหรับคนที่ต้องการจะทำการ Memory Debugging บนโปรแกรมที่เขียนขึ้นด้วยภาษา C หรือ C++ ได้บ้างนะครับ ถ้าต้องการข้อมูลเพิ่มเติมลอง อ่านจาก Reference ที่ขึ้นไว้ให้ เพราะผมเองก็ศึกษาจากพวกนี้แหละ มีอะไรผิดพลาด หรือต้องการแนะนำสามารถ Comment ไว้ได้เลยนะครับ ^ ^

Taxonomy upgrade extras: