C開發中段錯誤的三種調試方法
大家好,我是雜燴君。
嵌入式C開發,或多或少都遇到段錯誤(segmentation fault )。
段錯誤相比于總線錯誤,是一種更常見的錯誤。
段錯誤是怎么產生的呢?
段錯誤是因為訪問不可訪問的內存產生的。
下面是一些典型的段錯誤產生的原因:
- 訪問不存在的內存地址
- 訪問只讀的內存地址
- 棧溢出
- 內存越界
- ……
段錯誤實例
1、實例1:訪問不存在的內存地址
int main(int argc, char **argv)
{
printf("==================segmentation fault test==================\n");
int *p = NULL;
*p = 1234;
return 0;
}
2、實例2:訪問只讀的內存地址
int main(int argc, char **argv)
{
printf("==================segmentation fault test1==================\n");
char *str = "hello";
str[0] = 'H';
return 0;
}
3、實例3:棧溢出
static void test(void)
{
char buf[1024 * 1024] = {0};
static int i = 0;
i++;
printf("i = %d\n", i);
test();
}
int main(int argc, char **argv)
{
printf("==================segmentation fault test2==================\n");
test();
return 0;
}
4、實例4:內存越界
int main(int argc, char **argv)
{
printf("==================segmentation fault test3==================\n");
static char arr[5] = {0, 1, 2, 3, 4};
printf("arr[10000] = %d\n", arr[10000]);
return 0;
}
段錯誤調試方法
從上面的幾個例子中,我們應該對段錯誤有了一定的認識,但實際項目中,實際中,段錯誤可能沒有上面的例子那么明顯看出。如果之前沒有這方面的經驗,可能一時半會也定位不到問題。下面分享段錯誤的3種調試方法供大家參考。
我們依舊使用例子來說明,例子:
static void func0(void)
{
printf("This is func0\n");
int *p = NULL;
*p = 1234;
}
static void func1(void)
{
printf("This is func1\n");
func0();
}
int main(int argc, char **argv)
{
printf("==================segmentation fault test4==================\n");
func1();
return 0;
}
1、gdb一步步運行
使用gdb調試,打一些斷點、按流程運行下去,運行到段錯誤的地方會直接提示報錯。
或者使用命令行直接gdb調試:
這里我們是在x86上運行,如果是定位arm嵌入式Linux程序,我們怎么做的?
同樣也是可以使用gdb的,可以參考我們之前分享的文章:VSCode+gdb+gdbserver遠程調試ARM程序
2、通過core文件
Linux下,一個程序崩潰時,它一般會在指定目錄下生成一個core文件。core文件僅僅是一個內存映象(同時加上調試信息),主要是用來調試的。
core文件可打開與關閉。相關命令:
ulimit -c # 查看core文件是否打開
ulimit -c 0 # 禁止產生core文件
ulimit -c unlimited #設置core文件大小為不限制大小
ulimit -c 1024 #限制產生的core文件的大小不能超過1024KB
0代表關閉。下面我們打開它:
運行程序時,程序崩潰時,在程序目錄下會生成core文件,如:
調試core文件:
gdb test core
3、利用backtrace進行分析
void func0(void)
{
printf("This is func0\n");
int *p = NULL;
*p = 1234;
}
void func1(void)
{
printf("This is func1\n");
func0();
}
void func2(void)
{
printf("This is func2\n");
func1();
}
void dump(int signo)
{
void *array[100];
size_t size;
char **strings;
size = backtrace(array, 100);
strings = backtrace_symbols(array, size);
printf("Obtained %zd stacks.\n", size);
for(int i = 0; i < size; i++)
{
printf("%s\n", strings[i]);
}
free(strings);
exit(0);
}
int main(int argc, char **argv)
{
printf("==================segmentation fault test5==================\n");
signal(SIGSEGV, &dump);
func2();
return 0;
}
當程序發生段錯誤時,內核會向程序發送SIGSEGV信號。dump為SIGSEGV信號處理函數,其實現用到了execinfo.h里的兩個函數:
int backtrace(void **buffer,int size);
char ** backtrace_symbols (void *const *buffer, int size);
backtrace函數用于獲取當前線程的調用堆棧,獲取的信息將會被存放在buffer中,它是一個指針列表。參數 size 用來指定buffer中可以保存多少個void* 元素。函數返回值是實際獲取的指針個數,最大不超過size大小 在buffer中的指針實際是從堆棧中獲取的返回地址,每一個堆棧框架有一個返回地址。
backtrace_symbols將從backtrace函數獲取的信息轉化為一個字符串數組。參數buffer應該是從backtrace函數獲取的指針數組,size是該數組中的元素個數(backtrace的返回值)。函數返回值是一個指向字符串數組的指針,它的大小同buffer相同。
每個字符串包含了一個相對于buffer中對應元素的可打印信息。它包括函數名,函數的偏移地址,和實際的返回地址。注意:該函數的返回值是通過malloc函數申請的空間,因此調用者必須使用free函數來釋放指針。如果不能為字符串獲取足夠的空間函數的返回值將會為NULL。
以上就是本次介紹的三種定位段錯誤問題的方法,可以定位不同程度的問題。