在 Linux 下編譯及除錯 C 程式碼的簡易指南

daxixiong發表於2015-11-10

對於Linux下的C程式設計師來說,幾乎天天都會和Linux打交道。但在很多人的眼中,Linux是一個易用性極差、靠命令驅動的作業系統,根本無法與有著友好使用者介面的Windows相比。確實是這樣的,即使大家的程式是執行在Linux下,基於以下種種原因,我們的大部分工作還是在Windows下完成的:

第一,除了編譯除錯程式碼之外,每個程式設計師還有很多工作要做,像文件編寫、郵件傳送及回覆、PPT製作等,這些工作在Windows下做要更方便快捷一些。

第二,公司及專案組的資源有限,一般不會為每個開發人員配備一臺安裝有Linux的機器,而是大家共用一臺或少許幾臺Linux機器。在每臺機器上建立多個使用者,需要用來編譯或除錯程式的時候,大家用某個使用者登入上去。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

這樣,問題就來了:自己平時是在Windows下面辦公的,而自己編寫的程式的執行環境又是Linux的,如何從Windows切換到Linux呢?是不是要到專門的Linux機器上去編寫程式碼呢?我們如何在Linux下除錯程式呢?本文將一一道來。

到Linux下去編譯執行程式的步驟

只要在Windows下安裝一個叫做SecureCRT的軟體和一個叫做FileZilla的軟體,便可輕鬆實現Windows到Linux的切換。
SecureCRT是一款支援SSH(SSH1和SSH2)的終端模擬程式,簡單地說是Windows下登入Linux伺服器主機的軟體。FileZilla是一個免費開源的FTP軟體,分為客戶端版本和伺服器版本兩種,具備所有的FTP軟體功能。(編者注:SecureCRT 是版權軟體,建議使用開源的 Putty 替代,不過切記勿在搜尋引擎隨便搜尋下載。)

在使用SecureCRT和FileZilla之前,要確保有一臺安裝了Linux的機器處於執行狀態(一般說來,每個開發小組都會有專門用於測試程式的機器,可以在此機器上安裝Linux)。作者使用的Linux機器的IP地址為xx.xx.xx.xx,使用者名稱為zxin10,密碼為yyyy。

第一步:使用SecureCRT登入Linux

開啟SecureCRT軟體,在介面上輸入IP和使用者名稱,如圖1所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南
圖1 登入介面

然後,單擊圖1中的“Connect”,在出現的介面上輸入密碼,如圖2所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖2 密碼輸入介面

密碼輸入正確之後,便登入到了Linux系統下,如圖3所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖3 登入成功之後的介面

為了編譯自己的程式,我們需要建立自己的檔案存放目錄,如圖4所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖4 新建個人目錄

目錄建立成功之後,我們便可以轉到目錄中去看一下,如圖5所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖5 轉到新建目錄

此時,“萬事俱備,只欠東風”,我們接下來要做的工作是利用FileZilla軟體將自己在Windows下編寫的程式傳上去。

示例程式如下:

/**********************************************************************
* 版權所有 (C)2015, Zhou Zhaoxiong。
*
* 檔名稱:Hello.c
* 檔案標識:無
* 內容摘要:演示Windows下編寫的程式如何在Linux下執行
* 其它說明:無
* 當前版本:V1.0
* 作    者:Zhou Zhaoxiong
* 完成日期:201501028
*
**********************************************************************/
#include <stdio.h>

/**********************************************************************
* 功能描述:主函式
* 輸入引數:無
* 輸出引數:無
* 返 回 值:0-執行完畢
* 其它說明:無
* 修改日期            版本號     修改人                修改內容
* -------------------------------------------------------------------
* 201501028        V1.0     Zhou Zhaoxiong        建立
***********************************************************************/
int main()
{
    printf("Hello, world!/n");

    return 0;
}

第二步:使用FileZilla將程式碼上傳到Linux

將該“Hello.c”檔案存放在D盤的“Test”資料夾下,並啟動FileZilla,如圖6所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖6 啟動FileZilla之後的介面

在“主機(H)”中輸入IP地址,在“使用者名稱(U)”中輸入“zxin10”使用者名稱,在“密碼(W)”中輸入正確的密碼,“埠(P)”可不填寫而使用預設值,則可登入到Linux機器上去。登上去後,轉到“zhouzx”目錄下,並將“Hello.c”檔案傳上去,如圖7所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖7 上傳檔案之後的介面

此時,“Hello.c”檔案已經傳到了“zhouzx”目錄下,現在可以對該檔案進行編譯了。

第三步:在Linux上編譯和執行程式。

使用“gcc -g -o Hello Hello.c”命令對檔案進行編譯,如圖8所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖8 編譯之後的結果

可以看到,編譯成功之後,有“Hello”檔案生成。緊接著,執行“Hello”命令,便可看到程式的輸出結果,如圖9所示。

一份簡單的在 Linux下編譯及除錯 C 程式碼的指南

圖9 程式的輸出結果

以上便是將Windows下的程式放到Linux下去編譯和執行的全過程。這裡只是示例了簡單的程式,實際軟體開發專案中的程式要複雜很多,但基本操作流程都是類似的。當然,直接在Linux下編寫程式也是可以的,如可以利用Vi編輯器來寫程式。但由於易用性的原因,我認為,在Windows下編寫程式要更方便一點。大家要根據自己的習慣及專案組的要求來選擇合理的程式碼編寫的方式。

程式除錯示例—用gdb分析core檔案

在實際的軟體開發專案中,程式出現問題是在所難免的。遙想本人參加工作之後首次遇到程式的情景,至今還歷歷在目。之前的經驗告訴我,我們越是驚慌失措,問題就越是解決不了。我們要先讓自己平靜下來,然後再尋找解決程式問題的辦法。

在Linux下做開發的朋友,想必都與core檔案打過交道。當看到自己的程式執行之後出現core時,很多人都慌亂了,彷彿天快要塌下來一樣。其實,我們大可不必如此,只要我們掌握了用gdb除錯core檔案的辦法,依然可以很快定位程式問題,一舉將bug消滅掉。有關Linux core檔案的更多介紹,請閱讀此文

這裡以一個實際的程式為例,以用gdb分析core檔案為例介紹了Linux下程式除錯的方法,同時演示了常見gdb命令的操作方法。

在Linux下執行“ulimit –a”命令檢視程式執行出錯時是否會產生core檔案,命令執行的結果中有“core file size = 0”表示不會產生core檔案,此時要使用“ulimit -c 1000000”命令設定core檔案的大小。

示例程式

/**********************************************************************
* 版權所有 (C)2015, Zhou Zhaoxiong。
*
* 檔名稱:GdbDebug.c
* 檔案標識:無
* 內容摘要:Gdb命令演示程式
* 其它說明:無
* 當前版本:V1.0
* 作    者:Zhou Zhaoxiong
* 完成日期:20151008
*
**********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 資料型別重定義
typedef unsigned char       UINT8;
typedef signed   int        INT32;
typedef unsigned int        UINT32;

// 函式宣告
void Sleep(UINT32 iCountMs);
void PrintInfo(void);
INT32 main();

/**********************************************************************
* 功能描述:主函式
* 輸入引數:無
* 輸出引數:無
* 返 回 值:無
* 其它說明:無
* 修改日期        版本號     修改人            修改內容
* -------------------------------------------------------------------
* 20151008       V1.0     Zhou Zhaoxiong      建立
***********************************************************************/
INT32 main()
{
    PrintInfo();   // 在螢幕上輸出訊息

    return 0;
}

/**********************************************************************
 * 功能描述: 在螢幕上輸出訊息
 * 輸入引數: 無
 * 輸出引數: 無
 * 返 回 值: 無
 * 其它說明: 無
 * 修改日期            版本號            修改人           修改內容
 * ----------------------------------------------------------------------
 * 20151008            V1.0        Zhou Zhaoxiong        建立
 ************************************************************************/
void PrintInfo(void)
{
    UINT32 iLoopFlag = 0;
    UINT32 iSum      = 0;
    UINT32 iLen      = 0;
    UINT8 *pCtrStr   = NULL;

    iLen = strlen(pCtrStr);

    for (iLoopFlag = 0; iLoopFlag < iLen; iLoopFlag ++)      // 列印訊息iLen次
    {
        printf("PrintInfo: hello, world!/n");

        iSum = iSum + iLoopFlag;

        Sleep(10 * 1000);   // 每10s列印一次
    }

    return;
}

/**********************************************************************
* 功能描述: 程式休眠
* 輸入引數: iCountMs-休眠時間(單位:ms)
* 輸出引數: 無
* 返 回 值: 無
* 其它說明: 無
* 修改日期          版本號       修改人              修改內容
* ------------------------------------------------------------------
* 20151008         V1.0     Zhou Zhaoxiong          建立
********************************************************************/
void Sleep(UINT32 iCountMs)
{
    struct timeval t_timeout = {0};

    if (iCountMs < 1000)
    {
        t_timeout.tv_sec = 0;
        t_timeout.tv_usec = iCountMs * 1000;
    }
    else
    {
        t_timeout.tv_sec = iCountMs / 1000;
        t_timeout.tv_usec = (iCountMs % 1000) * 1000;
    }
    select(0, NULL, NULL, NULL, &t_timeout);   // 呼叫select函式阻塞程式
}

用gdb分析core檔案

在Linux上用“gcc -g -o GdbDebug GdbDebug.c”命令對程式進行編譯之後,執行“GdbDebug”命令,發現在當前目錄下出現了core檔案。利用gdb命令對core檔案進行分析的過程如下所示:

~/zhouzhaoxiong/zzx/GdbDebug> gdb GdbDebug core     -- 啟動gdb對core檔案的分析
GNU gdb (GDB) SUSE (7.3-0.6.1)
Copyright (C) 2011 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-suse-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/zhou/zhouzhaoxiong/zzx/GdbDebug/GdbDebug...done.
Core was generated by `GdbDebug'.
Program terminated with signal 11, Segmentation fault.
#0  0x00007f4a736f9812 in __strlen_sse2 () from /lib64/libc.so.6
(gdb) where          -- 檢視程式出問題的地方
#0  0x00007f4a736f9812 in __strlen_sse2 () from /lib64/libc.so.6
#1  0x000000000040061a in PrintInfo () at GdbDebug.c:64   -- 可以看到,在GdbDebug.c檔案的第64行出的問題
#2  0x00000000004005e5 in main () at GdbDebug.c:41
(gdb) b 41           -- 在GdbDebug.c檔案第41行設立斷點
Breakpoint 1 at 0x4005e0: file GdbDebug.c, line 41.
(gdb) b 64           -- 在GdbDebug.c檔案第64行設立斷點
Breakpoint 2 at 0x400611: file GdbDebug.c, line 64.
(gdb) info b         -- 顯示斷點資訊
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004005e0 in main at GdbDebug.c:41
2       breakpoint     keep y   0x0000000000400611 in PrintInfo at GdbDebug.c:64
(gdb) r              -- 執行GdbDebug
Starting program: /home/zhou/zhouzhaoxiong/zzx/GdbDebug/GdbDebug 

Breakpoint 1, main () at GdbDebug.c:41
41          PrintInfo();   // 在螢幕上輸出訊息
(gdb) n             -- 執行下一步

Breakpoint 2, PrintInfo () at GdbDebug.c:64
64              iLen = strlen(pCtrStr);
(gdb) p iLen        -- 列印(輸出)iLen的值
$1 = 0
(gdb) p iLoopFlag   -- 列印(輸出)iLoopFlag的值
$2 = 0
(gdb) c             -- 繼續執行     
Continuing.

Program received signal SIGSEGV, Segmentation fault.    -- 程式core掉了
0x00007ffff7ae9812 in __strlen_sse2 () from /lib64/libc.so.6
(gdb) q             -- 退出gdb
A debugging session is active.

        Inferior 1 [process 26640] will be killed.

Quit anyway? (y or n) y
~/zhouzhaoxiong/zzx/GdbDebug>

從以上分析可知,執行GdbDebug.c檔案的第64行時程式core掉了。此時仔細分析程式,發現pCtrStr指標為空。當對一個不存在的指標取長度時,由於找不到地址,程式便崩潰了。修改的辦法也非常的簡單,只需要讓pCtrStr指標指向具體的地址即可。

常見gdb命令操作示例

修改之後的程式碼如下:

/**********************************************************************
* 版權所有 (C)2015, Zhou Zhaoxiong。
*
* 檔名稱:GdbDebug.c
* 檔案標識:無
* 內容摘要:Gdb命令演示程式
* 其它說明:無
* 當前版本:V1.0
* 作    者:Zhou Zhaoxiong
* 完成日期:20151008
*
**********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 資料型別重定義
typedef unsigned char       UINT8;
typedef signed   int        INT32;
typedef unsigned int        UINT32;

// 函式宣告
void Sleep(UINT32 iCountMs);
void PrintInfo(void);
INT32 main();

/**********************************************************************
* 功能描述:主函式
* 輸入引數:無
* 輸出引數:無
* 返 回 值:無
* 其它說明:無
* 修改日期        版本號     修改人            修改內容
* -------------------------------------------------------------------
* 20151008       V1.0    Zhou Zhaoxiong       建立
***********************************************************************/
INT32 main()
{
    PrintInfo();   // 在螢幕上輸出訊息

    return 0;
}

/**********************************************************************
 * 功能描述: 在螢幕上輸出訊息
 * 輸入引數: 無
 * 輸出引數: 無
 * 返 回 值: 無
 * 其它說明: 無
 * 修改日期            版本號            修改人           修改內容
 * ----------------------------------------------------------------------
 * 20151008           V1.0         Zhou Zhaoxiong        建立
 ************************************************************************/
void PrintInfo(void)
{
    UINT32 iLoopFlag = 0;
    UINT32 iSum      = 0;
    UINT32 iLen      = 0;
    UINT8 *pCtrStr   = "hello, world!";  // 修改了這行程式碼

    iLen = strlen(pCtrStr);

    for (iLoopFlag = 0; iLoopFlag < iLen; iLoopFlag ++)      // 列印訊息iLen次
    {
        printf("PrintInfo: hello, world!/n");

        iSum = iSum + iLoopFlag;

        Sleep(10 * 1000);   // 每10s列印一次
    }

    return;
}

/**********************************************************************
* 功能描述: 程式休眠
* 輸入引數: iCountMs-休眠時間(單位:ms)
* 輸出引數: 無
* 返 回 值: 無
* 其它說明: 無
* 修改日期          版本號       修改人              修改內容
* ------------------------------------------------------------------
* 20151008         V1.0     Zhou Zhaoxiong          建立
********************************************************************/
void Sleep(UINT32 iCountMs)
{
    struct timeval t_timeout = {0};

    if (iCountMs < 1000)
    {
        t_timeout.tv_sec = 0;
        t_timeout.tv_usec = iCountMs * 1000;
    }
    else
    {
        t_timeout.tv_sec = iCountMs / 1000;
        t_timeout.tv_usec = (iCountMs % 1000) * 1000;
    }
    select(0, NULL, NULL, NULL, &t_timeout);   // 呼叫select函式阻塞程式
}

編譯並執行之後,程式正常,說明問題已被我們解決掉。下面是常見的gdb命令的操作示例:

~/zhouzhaoxiong/zzx/GdbDebug> gdb GdbDebug    -- 啟動gdb除錯
GNU gdb (GDB) SUSE (7.3-0.6.1)
Copyright (C) 2011 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-suse-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/zhou/zhouzhaoxiong/zzx/GdbDebug/GdbDebug...done.
(gdb) b 64     -- 在GdbDebug.c檔案第64行設立斷點
Breakpoint 1 at 0x400611: file GdbDebug.c, line 64.
(gdb) b 72     -- 在GdbDebug.c檔案第72行設立斷點
Breakpoint 2 at 0x400637: file GdbDebug.c, line 72.
(gdb) info b   -- 顯示斷點資訊
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400611 in PrintInfo at GdbDebug.c:64
2       breakpoint     keep y   0x0000000000400637 in PrintInfo at GdbDebug.c:72
(gdb) r        -- 執行GdbDebug
Starting program: /home/zhou/zhouzhaoxiong/zzx/GdbDebug/GdbDebug 

Breakpoint 1, PrintInfo () at GdbDebug.c:64
64              iLen = strlen(pCtrStr);
(gdb) p iLen    -- 列印(輸出)iLen的值
$1 = 0
(gdb) n         -- 執行下一步
66              for (iLoopFlag = 0; iLoopFlag < iLen; iLoopFlag ++)      // 列印訊息iLen次
(gdb) n         -- 執行下一步
68              printf("PrintInfo: hello, world!/n");
(gdb) p iLoopFlag   -- 列印(輸出)iLoopFlag的值
$2 = 0
(gdb) p iLen    -- 列印(輸出)iLen的值
$3 = 13
(gdb) n         -- 執行下一步
PrintInfo: hello, world!    -- 程式的輸出結果
70                      iSum = iSum + iLoopFlag;
(gdb) p iSum    -- 列印(輸出)iSum的值
$4 = 0
(gdb) n        -- 執行下一步

Breakpoint 2, PrintInfo () at GdbDebug.c:72
72                      Sleep(10 * 1000);   // 每10s列印一次
(gdb) n      
66              for (iLoopFlag = 0; iLoopFlag < iLen; iLoopFlag ++)      // 列印訊息iLen次
(gdb) p iLoopFlag
$5 = 0
(gdb) n
68              printf("PrintInfo: hello, world!/n");
(gdb) p iLoopFlag
$6 = 1
(gdb) n
PrintInfo: hello, world!
70                      iSum = iSum + iLoopFlag;
(gdb) p iSum
$7 = 0
(gdb) n

Breakpoint 2, PrintInfo () at GdbDebug.c:72
72                      Sleep(10 * 1000);   // 每10s列印一次
(gdb) p iSum
$8 = 1
(gdb) finish        -- 一直執行到函式返回
Run till exit from #0  PrintInfo () at GdbDebug.c:72
PrintInfo: hello, world!

Breakpoint 2, PrintInfo () at GdbDebug.c:72
72                      Sleep(10 * 1000);   // 每10s列印一次
(gdb) c           -- 繼續執行 
Continuing.
PrintInfo: hello, world!

Breakpoint 2, PrintInfo () at GdbDebug.c:72
72                      Sleep(10 * 1000);   // 每10s列印一次
(gdb) bt            -- 列印當前的函式呼叫棧的所有資訊
#0  PrintInfo () at GdbDebug.c:72
#1  0x00000000004005e5 in main () at GdbDebug.c:41
(gdb) q              -- 退出gdb
A debugging session is active.

        Inferior 1 [process 26685] will be killed.

Quit anyway? (y or n) y
~/zhouzhaoxiong/zzx/GdbDebug>

作為Linux下除錯C/C++程式的工具,大家一定要熟練掌握gdb的用法。

總結

Linux具有免費、可靠、安全、穩定、多平臺等特點,因此深受全球各大IT廠商的追捧。Linux作業系統的兩大主要應用領域是伺服器領域和嵌入式Linux系統。不管你從事的開發工作是否與Linux有關,掌握Linux下的軟體開發方法總是有好處的。

相關文章