抽絲剝繭帶你一步步解決程序死機崩潰的故障

1、程序死機,崩潰

        程序死機,崩潰這個應該是程序員調試代碼中經常遇到的問題,同時也是最難調試的一個問題。那麼什麼樣的現象是程序死機與崩潰呢?window系統的藍屏就是一種,指操作系統運行遇到了致命的錯誤,無法運行,只能關機重新上電。對於嵌入式軟件系統中,程序死機,崩潰也是程序運行遇到致命錯誤,無法運行。有的shell接口或命令行接口的系統,軟件中如果提前編寫了故障信息打印代碼,在發生死機時,會看到相關的打印信息,能夠根據打印信息來分析解決死機問題。本篇文章就是在一次實際調試程序死機時故障的記錄,通過本文,你將瞭解了怎麼通過一個打印信息來順藤摸瓜打到引起程序死機,崩潰的代碼,解決軟件調試中最經常遇到,也最難調試的一個故障。

2、什麼原因會導致死機

       對於嵌入式處理器,導致死機的軟件操作一般有,(1)、除法除數爲0;(2)、訪問非法內存(比如程序中對flash直接進行寫操作,就是把flahs的存儲空間當成了內存空間,直接進行了寫操作);(3)、各種外設的寄存器操作不正確導致的硬件故障;當出現這些故障時,處理器都會進入一個特殊的硬件錯誤中斷,在cortex-m3內核中就是hard fault中斷,這個中斷是一個死循環,爲的是捕獲這種致命的錯誤,使用戶可以發現程序發生了嚴重的故障。對於這種故障中斷髮生,如果你詳細瞭解cortex-m3內核的架構與原理,有一個快速的方法找到發生故障的代碼,那就是在故障中斷中查看堆棧寄存器的值,通過mem窗口查看堆棧寄存器中保存的內存地址開始的第7個字,就是發生故障的代碼。從反彙編窗口中輸入這第7個字的對應的代碼地址,查看此處的代碼是什麼,引發故障的代碼,仔細分析一下代碼就找到了問題。

 

3、內存泄漏導致的死機

       內存泄漏這個詞起的很高大上,用白話解釋一下就是,老王家的地和老趙家的地是鄰居,老王種地(土地比喻爲內存,種子比較爲寫入操作)時超過了自己家的地範圍,種到了老趙家的地裏,把老趙已經種好的地給破壞了。老王的種子泄漏到老趙家的土地裏,造成了內存泄漏。內存泄漏也是上段提到的內存非法操作一種。只不過是內存泄漏有時在剛剛發生泄漏時,並不會引起軟件的嚴重故障,軟件還能運行,當軟件運行到再次讀寫使用這段被改寫的內存纔可能引發死機。這時如果只是使用上段中所說的方法,相當時只是找到了作案現象,並不到抓到作案兇手。這種內存泄漏導致的死機是最最難解決的一種軟件死機,本文就從最難處理的問題入手,帶你一步步抓到作案兇手,給你提供一種軟件死機的解題思路。

 

4、軟件死機的故障現象

       這次介紹的軟件死機發生了rt thread操作系統中,操作系統軟件中故障信息打印,當出現故障時,只是一個斷言信息出現了 ,rt_free函數釋放一個內存出錯,並且打印出來斷言,程序停止在斷言中,捕捉到了故障發生的點。如下圖。

      

        從打印信息可知,rt_free釋放內存出錯誤,即釋放了一個非法的內存,這種現象只是在特殊的網絡通信情況下出現,平時運行並沒有出現,可以推斷代碼中rt_free()函數輸入的釋放內存值是合法的,如果是因爲程序代碼編寫錯誤,釋放內存只要一運行就會出現。出現錯誤的原因就是釋放的這段內存被其他程序段給非法改寫,導致rt_free釋放時,檢查出了內存被改寫,打印出了斷言。

4.1  找到被改寫的內存

       通過程序的打印信息可以知道,內存堆中的0x2000b478內存被改寫了,這個內存是哪個程序釋放的呢?首先我們要找到rt_free釋放的內存,因爲打印出來的這個內存並不是被應用程序釋放的內存。請看rt_free代碼。

/**
 * This function will release the previously allocated memory block by
 * rt_malloc. The released memory block is taken back to system heap.
 *
 * @param rmem the address of memory which will be released
 */
void rt_free(void *rmem)
{
    struct heap_mem *mem;

    if (rmem == RT_NULL)
        return;

    RT_DEBUG_NOT_IN_INTERRUPT;

    RT_ASSERT((((rt_uint32_t)rmem) & (RT_ALIGN_SIZE - 1)) == 0);
    RT_ASSERT((rt_uint8_t *)rmem >= (rt_uint8_t *)heap_ptr &&
              (rt_uint8_t *)rmem < (rt_uint8_t *)heap_end);

    RT_OBJECT_HOOK_CALL(rt_free_hook, (rmem));

    if ((rt_uint8_t *)rmem < (rt_uint8_t *)heap_ptr ||
        (rt_uint8_t *)rmem >= (rt_uint8_t *)heap_end)
    {
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("illegal memory\n"));

        return;
    }

    /* Get the corresponding struct heap_mem ... */
    mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);

    RT_DEBUG_LOG(RT_DEBUG_MEM,
                 ("release memory 0x%x, size: %d\n",
                  (rt_uint32_t)rmem,
                  (rt_uint32_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr))));


    /* protect the heap from concurrent access */
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);

    /* ... which has to be in a used state ... */
    if (!mem->used || mem->magic != HEAP_MAGIC)
    {
        rt_kprintf("to free a bad data block:\n");
        rt_kprintf("mem: 0x%08x, used flag: %d, magic code: 0x%04x\n", mem, mem->used, mem->magic);
    }
    RT_ASSERT(mem->used);
    RT_ASSERT(mem->magic == HEAP_MAGIC);
    /* ... and is now unused. */
    mem->used  = 0;
    mem->magic = HEAP_MAGIC;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(mem, "    ");
#endif

    if (mem < lfree)
    {
        /* the newly freed struct is now the lowest */
        lfree = mem;
    }

#ifdef RT_MEM_STATS
    used_mem -= (mem->next - ((rt_uint8_t *)mem - heap_ptr));
#endif

    /* finally, see if prev or next are free also */
    plug_holes(mem);
    rt_sem_release(&heap_sem);
}

     從上面代碼“mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);”以看出釋放的內存rmem和打印出來的內存控制塊mem之間是相差SIZEOF_STRUCT_MEM(12個字節),如下代碼,就是一個結構體的長度。那麼rmem的值就應該是mem+12,也就是說用戶申請到的內存mem的值和內存控制塊rmem之間相差12字節的。應用程序使用的內存地址應該是0x2000b478+0x0c(12) = 0x2000b484。

 

#define SIZEOF_STRUCT_MEM    RT_ALIGN(sizeof(struct heap_mem), RT_ALIGN_SIZE)
#define HEAP_MAGIC 0x1ea0
struct heap_mem
{
    /* magic and used flag */
    rt_uint16_t magic;
    rt_uint16_t used;

    rt_size_t next, prev;

#ifdef RT_USING_MEMTRACE
    rt_uint8_t thread[4];   /* thread name */
#endif
};

     上面程序的打印信息顯示rmem“0x2000b478”開始的第一個半字即結構體成員magic被修改了,magic正確的值應該爲HEAP_MAGIC(0x1EA0),現在是0x1E00,即rmem地址開始的第一個字節被其他程序修改了。

4.2   尋找引起改寫內存的代碼

     根據代碼結構分析,上述打印信息運行的在一個tcpcleinet的線程中,線程實現和服務器進行TCP雙向異步通信的功能。被改寫的內存很大可能是在這個線程中使用的動態分配內存或是靜態數組的。首先確定一下程序中內存堆的佔用的地址空間,確認是動態內存還是靜態數組。

    board.c文件中關於內存堆的初始化代碼如下,通過代碼可以看出內存堆使用的RAM空間範圍是HEAP_BEGIN(Image$$RW_IRAM1$$ZI$$Limit)--HEAP_END(STM32 RAM的結束地址,即0x2001 0000)。

/**
 * This function will initial STM32 board.
 */
void rt_hw_board_init(void)
{
    HAL_Init();
    SystemClock_Config();
#ifdef RT_USING_HEAP
    rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
#endif
#ifdef RT_USING_COMPONENTS_INIT
    rt_components_board_init();
#endif
#ifdef RT_USING_CONSOLE
    rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
}
相關的宏定義爲如下:
// </e>
// <o> Internal SRAM memory size[Kbytes] <8-64>
//	<i>Default: 64
#define STM32_SRAM_END (0x20000000 + STM32_SRAM_SIZE * 1024)
#ifdef __CC_ARM
extern int Image$$RW_IRAM1$$ZI$$Limit;
#define HEAP_BEGIN  ((void *)&Image$$RW_IRAM1$$ZI$$Limit)
#elif __ICCARM__
#pragma section="HEAP"
#define HEAP_BEGIN  (__segment_end("HEAP"))
#else
extern int __bss_end;
#define HEAP_BEGIN  ((void *)&__bss_end)
#endif
#define HEAP_END    STM32_SRAM_END

    那麼Image$$RW_IRAM1$$ZI$$Limit這個值代表的是什麼呢?先查看權威的解釋,來自mdk的幫助文件

      幫助文件的解釋爲,Image$$RW_IRAM1$$ZI$$Limit是IRAM ZI段的結尾,在ARM處理器的編譯映象佔用的內存中分配爲,RAM中先放置爲RW段(即讀寫段),再放置ZI(初始化全部爲0)。 ZI段後面的空間就是RAM的未用空間。這個 Image$$RW_IRAM1$$ZI$$Limit的意思就表示內部RAM中ZI段的超出結束位置地址,實際就是未使用的RAM空間的開始地址。這個地址的值實際應該是多少呢?只有在編譯,鏈接完成後才能看到,查看map文件即可找到,如下圖,即0x20002FB8。

     到此內存堆的使用的空間就是0x20002fb8-0x20010000,被異常改寫的內存地址0x2000B478正好在內存堆中,並且不在內存堆的邊界,這裏得出一個信息就是,這個被改寫的內存中由於用戶申請到的其他動態內存寫錯誤導致的。 代碼中申請動態內存的地方有幾千處,應該從哪裏入手查找呢?實現最有可能就是處於同一線程中的應用代碼中申請的內存。此程序在tcpclient線程中,查看代碼,通過仿真器在線查看所有用戶申請的內存地址和長度或者通過串口打印出來內存地址。

     pipe_buff:0x2000B2DC, 長度200字節,即佔用內存堆範圍爲0x2000B2DC-0x2000B3A3,包括使用的內存控制塊的12字節,佔用的空間爲0x2000B2D0-0x2000B3A3

     sock_buff:0x2000B3B0, 長度200字節,即佔用內存堆範圍爲0x2000B3B0-0x2000B477,包括使用的內存控制塊的12字節,佔用的空間爲0x2000B3A4-0x2000B477

     可以看出來,sock_buf的佔用的內存範圍和被改寫的地址0x2000B478,緊挨着,如果對sock_buff內存進行寫入操作發生一個字節的越界就是會改寫了0x2000B478這個地址,至此就找到原因,就是因爲對sock_buf的在某些情況下的寫入操作導致出現。

     問題的範圍現在已經被大大縮小到對一個變量內存的讀寫操作,剩下的工作就是查看所有操作sock_buf的代碼。關於此處代碼不多,僅有2處。如下。

static void select_handle(rt_tcpclient_t *thiz, char *pipe_buff, char *sock_buff)
{
    fd_set fds;
    rt_int32_t max_fd = 0, res = 0;

    max_fd = MAX_VAL(thiz->sock_fd, thiz->pipe_read_fd) + 1;
    FD_ZERO(&fds);

    while (1)
    {
        FD_SET(thiz->sock_fd, &fds);
        FD_SET(thiz->pipe_read_fd, &fds);

        res = select(max_fd, &fds, RT_NULL, RT_NULL, RT_NULL);

        /* exception handling: exit */
        EXCEPTION_HANDLE(res, "select handle", "error", "timeout");

        /* socket is ready */
        if (FD_ISSET(thiz->sock_fd, &fds))
        {
            res = recv(thiz->sock_fd, sock_buff, BUFF_SIZE, 0);

            /* exception handling: exit */
            EXCEPTION_HANDLE(res, "socket recv handle", "error", "TCP disconnected");

            /* have received data, clear the end */
            /*頤景園項目地信號不好,同時服務器針對這個設備的開關指令發送頻率過高,sock_buff(0x2000b3b0)導致會收到BUFF_SIZE(200)長度的數據,
              下面的操作就會意外的修改了其他的內存0x20000B478的magic_head,另外一個內存0x20000B478在free時發生斷言 zhaoshimin 20191110*/
            /*sock_buff[res] = '\0'; */

            RX_CB_HANDLE(sock_buff, res);

            
        }

        /* pipe is read */
        if (FD_ISSET(thiz->pipe_read_fd, &fds))
        {
            /* read pipe */
            res = read(thiz->pipe_read_fd, pipe_buff, BUFF_SIZE);

            /* exception handling: exit */
            EXCEPTION_HANDLE(res, "pipe recv handle", "error", "");

            /* have received data, clear the end */
            /*修改原因同上line 352行  zhaoshimin 20191110*/
            /*pipe_buff[res] = '\0';*/

            /* write socket */
            res = send(thiz->sock_fd, pipe_buff, res, 0);

            /* exception handling: warning */
            EXCEPTION_HANDLE(res, "socket write handle", "error", "warning");

            
        }
    }
exit:
    rt_free(pipe_buff);
    rt_free(sock_buff);
    
    /*關閉連接,釋放資源*/
    rt_tcpclient_close(thiz);
    
}

static void tcpclient_thread_entry(void *param)
{
    rt_tcpclient_t *temp = param;
    char *pipe_buff = RT_NULL, *sock_buff = RT_NULL;

    pipe_buff = rt_malloc(BUFF_SIZE);
    if (pipe_buff == RT_NULL)
    {
        LOG_E("thread entry malloc pipe buff error\n");
        return;
    }

    sock_buff = rt_malloc(BUFF_SIZE);
    if (sock_buff == RT_NULL)
    {
        rt_free(pipe_buff);
        LOG_E("thread entry malloc sock buff error\n");
        
        return;
    }

    memset(sock_buff, 0, BUFF_SIZE);
    memset(pipe_buff, 0, BUFF_SIZE);

    select_handle(temp, pipe_buff, sock_buff);
}

        從以上代碼(代碼已經改正錯誤)可以看出,res = recv(thiz->sock_fd, sock_buff, BUFF_SIZE, 0);用於讀取最多BUFF_SIZE(200)個字節到sock_buf內存中,這句沒有問題,下面的sock_buff[res] = '\0';這句就有問題,當讀取到的數據長度爲200,即res=200,再執行一次 sock_buff[200] = '\0',就發生了內存操作越界,改寫了其他內存。

       同理,pipe_buf的操作也存在同樣的問題,但是實際使用中對pipe_buff[res] = '\0';的操作不會出現res=200的情況,所有這句代碼就從來沒有發現過內存操作越界的,但是代碼有問題也一同更改。

4.3   改正代碼修復bug

       找到問題代碼,修改起來就很容易了,就是註釋掉這兩句內存操作越界的代碼。

5、調試總結

       本文由表及裏,深入淺出的完整描繪了一次實際項目經驗中遇到的重大軟件bug,這個軟件bug是在時間很緊,壓力很大的情況下解決的。整個bug的解決思路融入着12年嵌入式軟件開發調試經驗,一次很寶貴經驗分享。如果讀者想通過此篇文章開闊一下思路,提高一下調試技能,要多看幾遍,才能理解這種思路。

     

發佈了37 篇原創文章 · 獲贊 79 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章