1. 程式人生 > >Char型別數值超出範圍,導致程式陷入死迴圈深入分析

Char型別數值超出範圍,導致程式陷入死迴圈深入分析

一、問題

        本段程式碼有什麼問題?如何修改?

#include <iostream>
using namespace std;

#define MAX 255

main()
{
      char p[MAX+1];
      char ch;
      
      for (ch=0;ch<=MAX;ch++)
      {
          p[ch]=ch;
          cout<<ch<<" ";
          }
      cout<< ch<<" ";
}
解析:

首先這段程式在執行時會陷入死迴圈,歸其原因是程式碼中“char數值問題”。具體分析如下。

       1、char取值範圍

       char型別佔一位元組(8位),取值範圍為-128到127。如何得到的取值範圍呢?具體如下:

       由於數字在計算機中是以補碼形式表示,並且char型別為有符號數,則一位元組(8位)中的最高位為符號位。能表示的最大正數二進位制形式為“01111111”,由於正數補碼與原碼相同,其原碼同樣為“01111111”,對應十進位制為127;同理,能表示的最小負數二進位制形式為“10000000”,由於這是補碼形式,其原碼對應為“110000000”,對應十進位制為-128。

       2、知道了char的取值範圍,回過頭看題目中程式碼的for迴圈語句,ch的取值從0到127這階段的迴圈語句都很正常。當ch=128時,由於超出char取值範圍,編譯器會將ch值變為-128,仍然滿足<=MAX條件,繼續執行迴圈,同時ch++

。由於每次for迴圈ch都自增(ch++),ch值將會逐漸從-128遞增到0,這時又回到了for迴圈的起始條件處(ch=0),當再次遞增到127後,ch值又會變為-128,以後不斷迴圈。

       3、從以上分析中可以看出,“ch++”語句並沒有使ch值一直增加,相反ch值不斷在“-128到127”中迴圈變化,進而是“ch<=MAX”(MAX=255)永遠成立,程式陷入死迴圈。

       下面,為了探究其根本原因,對程式進行除錯,從編譯器角度分析程式陷入死迴圈原因。

深入分析:

       主要看程式中“for (ch=0;ch<=MAX;ch++)”和“p[ch]=ch;”語句的彙編程式碼:

for (ch=0;ch<=MAX;ch++)
0040116E   mov         byte ptr [ebp-104h],0
00401175   jmp         main+35h (00401185)
00401177   mov         al,byte ptr [ebp-104h]
0040117D   add         al,1
0040117F   mov         byte ptr [ebp-104h],al
00401185   movsx       ecx,byte ptr [ebp-104h]
0040118C   cmp         ecx,0FFh
00401192   jg          main+7Ch (004011cc)
{
         p[ch]=ch;
00401194   movsx       edx,byte ptr [ebp-104h]
0040119B   mov         al,byte ptr [ebp-104h]
004011A1   mov         byte ptr [ebp+edx-100h],al
 從彙編程式碼中可知,edx暫存器中存放的是ch值,[ebp+edx-100h]對應“char型陣列p”下標。通過除錯程式可知p[0]記憶體地址為0x0013fe80,p[1]記憶體地址為0x0013fe81,p[2]記憶體地址為0x0013fe82,……p[127]記憶體地址為0x0013feff。p[0]至p[127]記憶體佈局如下圖所示(其中127的十六進位制表示為“0x7F”):


p[0]到p[127],一切都很正常。當執行ch++後ch=128 時,就會出現問題,繼續看彙編程式碼。

  for (ch=0;ch<=MAX;ch++)
00401177   mov         al,byte ptr [ebp-104h]  //此時[ebp-104h]地址為0x13fe7c,對應資料為“0x7F”。
0040117D   add         al,1
0040117F   mov         byte ptr [ebp-104h],al  //執行自增操作後,結果寫入記憶體[ebp-104h]地址處,也就是
                                                 說0x13fe7c處內容將變為“0x80”。
00401185   movsx       ecx,byte ptr [ebp-104h] //將記憶體[ebp-104h]地址處內容符號擴充套件後傳入ecx暫存器。

看下執行這條語句後ecx暫存器中內容:


對“0x80”進行符號擴充套件後變為“0xFFFFFF80”。這裡有必要對movsx進行下說明,

movsx:先符號位擴充套件,再傳送。以“0x80”為例,二進位制形式為“10000000”,符號擴充套件成32位後為“11111111111111111111111110000000”,十六進位制為“0xFFFFFF80”。與movsx相對的有movzx,movzx:先零擴充套件再傳送。

       這時ecx暫存器中的資料其值的十進位制已變為-128。繼續往下看彙編程式碼。

{
       p[ch]=ch;
0040118C   cmp         ecx,0FFh //-128確實小於255,執行迴圈體
00401194   movsx       edx,byte ptr [ebp-104h] //同樣將將記憶體[ebp-104h]地址處內容符號擴充套件後傳入edx寄
                                                 存器。
0040119B   mov         al,byte ptr [ebp-104h] 
004011A1   mov         byte ptr [ebp+edx-100h],al //由於edx的內容為“-128”,此時[ebp+edx-100h]地址為
                                                    0x0013fe00。
       因此,al暫存器中的資料“0x80”將被寫入記憶體0x0013fe00地址處。


寫入資料之後,程式再繼續往下執行,由於資料寫入的位置超出了程式之前給p[]申請的棧空間,而這部分記憶體空間之前的內容是cout函式地址。並且,在程式執行迴圈體中的“cout<<ch<<" "”程式碼時,會被用來儲存臨時資料,所以之前寫入的p[ch]值,在執行一次for迴圈後會被清除掉,且其地址處的內容會恢復到程式開始時存入的cout函式地址。

       此時,ch=-128,繼續執行for迴圈,同時ch自增(ch++),ch的值會從-128變到0,之後程式又會正常地將p[ch]值從記憶體地址0x0013fe80處開始依次寫入,當ch增到127後,由於編譯器的movsx操作又會將ch值變為-128,陷入死迴圈。

總結:

       從編譯器角度分析,出現這種迴圈的根本原因是,編譯器使用“movsx”操作傳值,由於ch是char型,當其值在0至127之間時,對應二進位制數最高位為0,也就是符號位為正,使用movsx符號擴充套件後仍然為正,並且高位用都用0填充。當ch值大於等於128時,對應二進位制最高位為1,也就是符號位為負,符號位擴充套件後仍然為負,並且高位都用1填充,這就是為什麼ch自增到128時,編譯器將將其值變為-128的根源。

二、問題進階

如前面所述,char範圍為-128至127,若將程式改成unsigned char型,還會陷入死迴圈嗎?修改後的程式碼:

#include <iostream>
using namespace std;

#define MAX 255

main()
{
      char p[MAX+1];
      unsigned char ch;  //修改成無符號char型
      
      for (ch=0;ch<=MAX;ch++)
      {
          p[ch]=ch;
          cout<<ch<<" ";
          }
      cout<< ch<<" ";
}
   執行此程式,同樣陷入死迴圈。Why?先來看下for迴圈部分的彙編程式碼:
for (ch=0;ch<=MAX;ch++)
0040116E   mov         byte ptr [ebp-104h],0
00401175   jmp         main+35h (00401185)
00401177   mov         al,byte ptr [ebp-104h]
0040117D   add         al,1
0040117F   mov         byte ptr [ebp-104h],al
00401185   mov         ecx,dword ptr [ebp-104h]
0040118B   and         ecx,0FFh
00401191   cmp         ecx,0FFh
00401197   jg          main+86h (004011d6)
{
     p[ch]=ch;
00401199   mov         edx,dword ptr [ebp-104h]
0040119F   and         edx,0FFh
004011A5   mov         al,byte ptr [ebp-104h]
004011AB   mov         byte ptr [ebp+edx-100h],al
           可以看出將ch設定成unsigned char後的彙編程式碼與設定成char的彙編程式碼有少量不同。在unsigned char版本中編譯器沒有使用movsx指令。在ch與MAX作比較時,char版本使用“movsx       ecx,byte ptr [ebp-104h]”程式碼對[ebp-104h]地址取一位元組放入ecx暫存器中,而unsigned char版本使用“mov         ecx,dword ptr [ebp-104h]”程式碼對[ebp-104h]地址取一雙字放入ecx暫存器中。

由於設定成unsigned char型別,此時ch取值範圍為0至255。在char版本中編譯器使用“movsx”進行符號擴充套件來限制char的取值範圍,而在unsigned char版本中編譯器只是確保unsigned char型別佔一位元組。

當ch=255時,

       執行“00401185movecx,dword ptr [ebp-104h]”        後ecx暫存器儲存的是“0xCCCCCCFF”,

       執行“0040118Bandecx,0FFh”                                      後 ecx暫存器儲存的是“0xFF”,判斷等於MAX                                                                                                      執行迴圈體內賦值程式碼,

       執行到“00401199   mov         edx,dword ptr [ebp-104h]”   後edx暫存器儲存的是“0xCCCCCCFF”,

       執行“0040119F   and         edx,0FFh”                                      後edx暫存器儲存的是“0xFF”
       執行到“004011AB   mov         byte ptr [ebp+edx-100h],al” 後[ebp+edx-100h]的地址為“0x0013ff7f”

此時[ebp-104h]處儲存的是“0xFF”繼續執行下一輪for迴圈。

       執行“00401177   mov         al,byte ptr [ebp-104h]
                  0040117D   add         al,1
                  0040117F   mov         byte ptr [ebp-104h],al”程式碼後,[ebp-104h]處儲存資料將變為“0x00”,之後又會從ch=0開始執行for迴圈,程式再一次陷入死迴圈。

三、正確版本

       其實,把char改為unsigned char後,還需要把for迴圈中的判斷語句改成ch<MAX即可。正確程式碼如下:

#include <iostream>
using namespace std;

#define MAX 255

main()
{
      char p[MAX+1];
      unsigned char ch;  //修改成無符號char型
      
      for (ch=0;ch<MAX;ch++)
      {
          p[ch]=ch;
          cout<<ch<<" ";
          }
	  p[ch] = ch;
      cout<< ch<<" ";
}

     【注】本文目的在於通過一個題目的深入分析,瞭解編譯器在處理char與unsigned char的不同之處。就題目本身而言,修改方法很多,但通過本文的思維方式,可以深入到編譯器層面瞭解陷入死迴圈的根本原因。此篇內容是對《程式設計師面試寶典(第二版)》P.208 面試例題3的深入思考。