for 循環和while循環區別

C語言提供了好幾種循環結構,即while、for和do-while。彙編語言中並沒有相應的指令存在,作為替代,將條件測試和跳轉組合起來實現循環的效果。大多數彙編器根據一個循環的do-while形式來產生循環代碼,即使在實際程序中這種形式用的相對較少。其它的循環會首先轉換成do-while形式,然後再編譯成機器代碼。

do-while循環

其通用形式是這樣的:

do
body-statement
while (test-expr);

循環的效果就是重複執行body-statement,對test-expr求值,如果求值的結果為非零,就繼續循環。注意,body-statement至少執行一次。

do-while的通用形式可以翻譯成如下所示的條件和goto語句:

loop:

 body-statement
t = test-expr;
if(t)
goto loop;

也就是說每次循環程序會執行循環體裡面的語句,然後執行測試表達式。如果測試為真,則回去再執行一次循環。

下面示例用do-while循環計算函數參數的階乘,寫作n!只計算n>0時候n階乘的值:

int fact_do(int n)
{
int result = 1;
do {
result *= n;
n = n - 1;
}while(n > 1);
return result;
}

for 循環和while循環區別

彙編代碼是do-while循環的一個實現形式,這裡用的gcc編譯器

gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04)

編譯參數是

$ gcc -m32 -O2 -o fact

因為非常不習慣AT&T彙編形式,所以這裡用IDA pro 對得到的fact文件進行反彙編分析,原文的彙編形式(用edx保存參數n)我無論怎麼調節參數都無法得到。圖中是一個do-while循環的標準實現,eax初始化為1,epb+8地址處保存著參數n,0x08048404 處把參數n減一,緊接著0x08048408 處把n與1比較。如果為真則在0x0804840C處跳回循環的開始,這裡是循環的關鍵地方由它來判斷循環是繼續還是退出。

綜合0x080483F3,0x080483FA我們可以看到eax被初始化為1,在0x080483FD被乘法更新。如果學過x86彙編語言就知道 mul 乘法指令是離不開eax寄存器的,而且返回值通常也用eax寄存器。所以這裡eax對應於結果result是無懸念的。

理解產生的彙編代碼與原始代碼之間的關係,關鍵是找到程序值和寄存器之間的映射關係。對於循環fact_do來說,這個任務非常簡單,但是對於更復雜的程序來說,就可能是更具挑戰性的任務。C語言編譯器常常會重組計算,因此有些C代碼中的變量在機器代碼中沒有對應的值;而有時,機器代碼中又會引入源代碼中不存在的新值。此外編譯器還常常試圖將多個程序值映射到一個寄存器上,來最小化寄存器的使用率。  

上面的fact_do的過程對於逆向工程循環來說,是一個通用的策略。看看在循環之前如何初始化寄存器,在循環中如何更新和測試寄存器,以及在循環之後又如何使用寄存器。這些步驟中的每一步都提供了一個線索,組合起來就可以解開謎團。做好準備,你會看到令人驚奇的變換,其中有些情況很明顯是編譯器能夠優化的代碼,而有些情況很難解釋編譯器為什麼要選用那些奇怪的策略。

while循環

while語句的通用形式如下:

while(test-expr)
body-statement

與do-while不同的是,它對test-expr求值,在第一次執行body-statement之前,循環就可能中止。將while循環翻譯成機器代碼有很多種方法。一種常見的方法,也就是GCC採用的方法,是使用條件分支,在需要時省略循環體的第一次執行,從而將代碼轉換成do-while循環,如下:

if(!test-expr)
goto done;
do
body-statement
while(test-expr);
done:

接下來這個代碼可直接翻譯成goto代碼,如下:

 if t = test-expr
if(!t)
goto done;
loop:
body-statement
t = test-expr;
if(t)
goto loop;
done:

使用這種策略,編譯器常常會優化最開始的測試,比如說認為總是滿足測試條件。

舉個例子fact_while是使用while循環的階乘函數的實現,這個函數能正確的計算 0!=1 。fact_while_goto是GCC產生的彙編代碼的C語言翻譯,比較fact_do 和fact_while 我們看到它們幾乎是相同的。將while循環轉換成do-while循環,以及將後者翻譯成goto代碼。

int fact_while(int n)
{
int result = 1;
while(n > 1){
result *= n;
n = n - 1;
}
return result;
}
int fact_while_goto(int n)
{
int result = 1;
if(n <= 1)
goto done;
loop:
result *= n;
n = n - 1;
if(n > 1)
goto loop;
done:
return result;

}
for 循環和while循環區別

for循環

for循環的通用形式如下

for(init-expr;test-expr;update-expr)
body-statement

C語言標準說明,這樣一個循環的行為與下面這段使用while循環代碼的行為一樣:

init-expr;
while(test-expr) {
body-statement
update-expr;
}

程序首先對初始表達式init-expr求值,然後進入循環;在循環中它先對測試條件test-expr求值,如果測試結果為“假”就會退出,否則執行循環體body-statement;最後對更新表達式update-expr求值。

這段代碼編譯後的形式,基於前面講過的從while到do-while的轉換,首先給出do-while的形式:

init-expr;
if(!test-expr)
goto done;
do{
body-statement
update-expr;
}while(test-expr);
done:

然後將它轉換成goto代碼:

 init-expr;
t = test-expr
if(!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if(t)
goto loop;
done:

作為一個示例,考慮用for循環寫的階乘函數:

int fact_for(int n)
{
int i;
int result = 1;
for(i = 2;i <= n; i ++)
result *= i;
return result;
}

如上述代碼所示,用for循環編寫階乘函數最自然的方式就是將從2一直到n的因子乘起來,因此這個函數與我們使用while或者do-while循環的代碼都不一樣。

這段代碼中for循環的不同組成部分如下:

for 循環和while循環區別

用這些部分帶入前面給出的模板中的相應位置,得到下面goto代碼的版本:

int fact_for_goto(int n)
{
int i = 2;
int result = 1;
if( !(i <=n ) )
goto done;
loop:
result *= i;
i ++;
if(i <= n)
goto loop;
done:
return result;
}

確實仔細查看GCC產生的彙編代碼會發現非常接近如下形式:

for 循環和while循環區別

綜上所述,C語言中三種形式的所有循環— do-while,while和for–都可以用一種簡單的策略來翻譯,產生包含一個或多個條件分支的代碼。控制的條件轉移為循環翻譯成機器代碼提供了基本機制。

死循環選擇for還是while

最後再說一下,看到有人在網上討論死循環用 for(;;); 好,還是用 while(1); 好。

自己親自測試了下,在 -O2 參數下它們生成的彙編指令是一樣的(看來這應該跟優化配置和編譯器選擇有很大關係)。

for 循環和while循環區別


分享到:


相關文章: