本節必須掌握的知識點:
示例五源代碼
代碼分析
匯編解析
2.4.1?示例五
■格式化輸入函數scanf
scanf函數可以從鍵盤讀取輸入的信息。scanf函數同樣可以像printf函數那樣,通過轉換說明“%d”來限制函數只能讀取十進制數。scanf函數的參數為可變參數,參數的個數由格式化說明符的個數決定,可以同時接受鍵盤輸入多個值。scanf函數以回車符、制表符或者空格表示輸入結束。
示例代碼五
/*
顯示并確認輸入的整數值
*/
#include?<stdio.h>
#include?<stdlib.h>
int?main(void)
{
int?num;
printf("請輸入一個整數:");
scanf_s("%d", &num);//注意,scanf函數讀取變量時,變量名前必須加&
printf("您輸入的整數是%d。\n",?num);
system("pause");
return?0;
}
●輸出結果:
請輸入一個整數:12
您輸入的整數是12。
提示
1.?scanf函數讀取變量時,變量名前必須加&地址符,表示接收鍵盤輸入的值存儲到該地址處。
2.使用scanf函數,高版本的VS會顯示錯誤提示信息如下:
error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
1>c:\program files (x86)\windows
kits\10\include\10.0.18362.0\ucrt\stdio.h(1275): note:?參見“scanf”的聲明
解決方法:
方法1:在程序最前面加宏定義:
#define_CRT_SECURE_NO_DEPRECATE
方法2:在程序最前面加:
#pragma warning(disable:4996)
方法3:把scanf改為scanf_s;推薦使用的方法。
方法4:無需在程序最前面加宏定義,只需在新建項目時取消勾選“SDL檢查”即可。
方法5:若項目已建立好,在項目屬性里關閉SDL。
方法6:在工程項目設置一下就行:將報錯的宏定義放到:“項目屬性”>“C/C++”?>?“預處理器”>?“預處理器定義”。
方法7:在“項目屬性”>“C/C++?”>“命令行”中添加:/D _CRT_SECURE_NO_WARNINGS就可以了。
3.scanf_s()?函數并不是C標準庫函數,而是VS提供的函數,其功能雖然與scanf()?相同,但卻比?scanf()?安全,因為?scanf_s()?是針對“?scanf()在讀取字符串時不檢查邊界,可能會造成內存泄露”這個問題設計的。
scanf_s()用于讀取字符串時,必須提供一個數字以表明最多讀取多少位字符,以防止溢出。例如,scanf_s("%s", buf, 5);
當scanf_s()讀取非字符串時,無需考慮內存溢出問題。例如,scanf_s("%d", &num);
2.4.2?代碼分析
int?num;
第一步:聲明一個int類型的變量num,未初始化。
printf("請輸入一個整數:");
第二步:調用輸出函數printf打印提示信息,提示用戶輸入一個整數值,這是非常人性化的設計。
scanf_s("%d", &num);//注意,scanf函數讀取變量時,變量名前必須加&
第三步:調用scanf_s函數,接收鍵盤輸入,格式化說明符’%d’表示接收輸入一個32位整數值,并且存儲到num偏移地址處。&no表示取變量num的地址。
printf("您輸入的整數是%d。\n",?num);
第四步:再次調用printf函數輸出結果。
2.4.3?匯編解析
■匯編代碼
;FileName:2-4-1.asm
;例5:示例代碼4-1顯示并確認輸入的整數值
;by:bcdaren
;2023.08.27
;==================
;C標準庫頭文件和導入庫
include vcIO.inc
.data?;全局區
num sdword ?;全局變量
.const??;常量區
szMsg1 db "請輸入一個整數:",0?????
szMsg2 db "%d",0
szMsg3 db "您輸入的整數是%d。",0dh,0ah,0
.code?;代碼區
start:
;提示信息
push offset szMsg1?;格式化常量字符串偏移地址入棧
call printf?;調用printf函數輸出結果
;接收鍵盤輸入
lea esi,num?;取變量num地址
push esi
push offset szMsg2
call scanf
;輸出結果
invoke printf,offset szMsg3,num
;?????
invoke _getch;等待輸入單個字符
ret;結束返回
end start
上述匯編代碼中引用了C標準庫函數scanf,在vcIO.inc頭文件中已聲明。因為沒有使用VS編譯器,因此這里不可以使用scanf_s函數。調用scanf函數前,先使用lea指令取變量num的地址,然后入棧,接著將格式化常量字符串szMsg2入棧,最后call指令調用scanf函數,接收鍵盤輸入一個整數值,并存儲到no偏移地址處。
■反匯編代碼
int num;
printf("請輸入一個整數:");
00E61952?push offset string
"\xc7\xeb\xca\xe4\xc8\xeb\xd2\xbb\xb8\xf6\xd5\xfb\xca\xfd\xa3\xba" (0E67B30h)?
00E61957?call?_printf (0E6104Bh)?
00E6195C?add?esp,4?
scanf_s("%d", &no);//注意,scanf函數讀取變量時,變量名前必須加&
00E6195F?lea eax,[num]?
00E61962?push eax?
00E61963?push offset string "%d" (0E67B44h)?
00E61968?call _scanf_s (0E61154h)?
00E6196D?add esp,8?
printf("您輸入的整數是%d。\n", num);
00E61970?mov eax,dword ptr [num]?
00E61973?push eax?
00E61974?push????????offset string
"\xc4\xfa\xca\xe4\xc8\xeb\xb5\xc4\xd5\xfb\xca\xfd\xca\xc7%d\xa1\xa3\n" (0E67B48h)?
00E61979?call _printf (0E6104Bh)?
00E6197E?add esp,8?
2-4-1.c的反匯編代碼中,編譯器調用的是scanf_s函數,先用lea指令取變量no地址,然后兩個push參數入棧,與匯編代碼一樣。
此外,反匯編代碼中,調用printf函數時的第一個參數格式化常量字符串對應的是中文機內碼,可以參閱《X86匯編語言基礎教程》第四章常用編碼規則講述的漢字編碼規則。
實驗十七:驗證整數常量值作為指令操作數存儲在代碼段中
第一步:打開DtDebug調試器。
第二步:將匯編代碼生成的2-3-1.exe程序拖入調試器。
第三步:按Ctrl+F9,進入程序的入口地址,即程序代碼段的起始位置。
第四步:找到整數常量值的賦值語句,如圖2-9所示。
????????????????圖2-9?整數常量值
結論
【注意】紅色方框內的語句,右邊為常量值的賦值語句MOV DWORD PTR DS:[1E3010],2
意思為將整數常量值2存入數據段的偏移地址0x001E3010地址處。我們再看左側匯編指令對應的硬編碼,05C7為MOV DWORD PTR指令機器碼,001E3010為數據段內的偏移地址,00000002為整數常量值,作為該條機器指令的一部分存儲在代碼段中。注意X86 CPU字節為單位的小端存儲方式,低地址為低字節數據,高地址為高字節數據。
實驗十八:乘法運算
VS新建項目2-4-2.c:
/*
讀取一個整數并顯示其3倍的值
*/
#include?<stdio.h>
#include?<stdlib.h>
int?main(void)
{
int?num;
printf("請輸入一個整數:");
scanf_s("%d", &num);//讀取整數值
printf("它的3倍的值是%d\n", 3 *?num);
system("pause");
return?0;
}
●輸出結果:
請輸入一個整數:11
它的3倍的值是33
請按任意鍵繼續. . .
練習
1、請讀者將2-4-2.c翻譯成匯編語言實現。
2、請讀者分析2-4-2.c的反匯編代碼。
實驗十九:輸出函數puts
VS新建項目2-4-3.c:
/*
顯示讀取到的兩個整數的和
*/
#include?<stdio.h>
#include?<stdlib.h>
int?main(void)
{
int?n1,?n2;
//puts函數功能:輸出字符串并換行,等同于printf("...\n")
puts("請輸入兩個整數。");
printf("整數1:");scanf_s("%d", &n1);
printf("整數2:");scanf_s("%d", &n2);
printf("它們的和是%d。\n",?n1?+?n2);??//顯示和
system("pause");
return?0;
}
●輸出結果:
請輸入兩個整數。
整數1:1
整數2:2
它們的和是3。
請按任意鍵繼續. . .
【注意】puts函數只有一個參數,僅用于輸出零結尾字符串。
練習
1、請讀者將2-4-3.c翻譯成匯編語言實現。
2、請讀者分析2-4-3.c的反匯編代碼。
實驗二十:顯示讀取到的兩個整數的和
VS新建項目2-4-4.c:
/*
顯示讀取到的兩個整數的和
*/
#include?<stdio.h>
#include?<stdlib.h>
int?main(void)
{
int?n1,?n2;
int?sum;//和
puts("請輸入兩個整數:");
printf("整數1:");scanf_s("%d", &n1);
printf("整數2:");scanf_s("%d", &n2);
sum?=?n1?+?n2;
printf("它們的和是%d。\n",?sum);//顯示和;
system("pause");
return?0;
}
●輸出結果:
請輸入兩個整數:
整數1:1
整數2:2
它們的和是3。
請按任意鍵繼續. . .
【注意】實驗十九和實驗二十的區別,實驗二十定義了一個int類型變量sum,用于保存變量n1+n2的和。當我們需要多次使用變量和的情況下,應該定義一個sum變量保存變量和。如果只打印一次,則不需要定義變量sum,可以根據實際需要,靈活設計。
總結
1.計算機的內存是一個以字節為單位的線性存儲空間,每個字節都有一個獨立的地址編號。計算機程序通過內存地址讀寫該地址相應的存儲空間。
2.源程序中的變量和常量都是存儲在內存中的數據。全局變量存儲在全局區,局部變量存儲在棧區,字符串常量存儲在常量區。整數常量或字符常量作為機器指令的操作數存儲在代碼區,可以直接引用,無需地址編號。
3.變量名就是用符號表示的地址編號。全局變量是相對于全局區內的偏移地址,局部變量是相對于棧區的偏移地址,常量字符串是相對于常量區的偏移地址。
4.對變量的引用:
在匯編語言中,使用mov指令引用變量時,表示從該變量偏移地址處取該地址存儲的值。例如,mov eax,[a](與mov eax,a等價)表示將變量a地址處的值送入eax寄存器。如果取變量的地址,則使用lea指令。例如,lea esi,a表示將變量a的地址送入esi寄存器。
在C語言中,沒有辦法通過指令來區分取變量地址還是取值,因此變量名表示該地址處存儲的值,變量名前添加地址符&表示變量地址,地址前添加*號(解引用運算符)表示取該地址處的值。后面我們將要學習的指針就是地址的意思,從匯編的角度理解指針是一件再簡單不過的事情了。
例如:
a = 1;該語句執行后,變量名a表示變量的值為1。而這條語句的真實含義是指將整數常量值1存儲到變量a偏移地址處,即&a地址處。
*(&a) = 1;這條語句的含義是將整數常量值1送入變量a地址處,等價于語句a = 1;這里的*號表示解引用,即表示變量a地址處的值等于1。
5.常量字符串:
在匯編代碼中,常量字符串需要使用一個符號表示常量字符串在常量區內的偏移地址。例如示例五匯編代碼中的szMsg1,使用操作符offset取szMsg1的偏移地址:offset szMsg1。
取出常量字符串的偏移地址就可以對字符串進行讀操作了。
如果字符串存儲到全局區或者棧區,那么該字符串就變成了變量了,一個以ASCII字符組成的數組,不僅可以對字符串進行讀操作,還可以對字符串進行寫操作。我們將在第十一章字符和字符串章節中詳細講解。
在C語言中,常量字符串存儲在常量區,采用直接引用的方式對常量字符串進行操作。例如puts("請輸入兩個整數:");puts函數的唯一參數是一個常量字符串,可以直接對其引用。但是記住,對常量字符串的引用其實是直接引用該常量字符串的在常量區內的偏移地址。
何以證明?請看puts語句對應的反匯編語句:
push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xc1\xbd\xb8\xf6\xd5\xfb\xca\xfd\xa3\xba" (0F27B30h)
offset string就表示該常量字符串的偏移地址(0F27B30h)。
6.const是C語言編譯器的修飾詞,用于修飾變量。例如const int j;表示在變量j的存續期間,不可以修改變量j的值,把變量j當作是常量看待,其實并沒有改變變量j的真實屬性,即變量j并不會因此而變成一個常量,不會改變變量j的存儲區。編譯器在編譯源程序時會檢查const修飾過的變量的值是否被修改,如果被修改,則會提示錯誤信息error C2166:?左值指定?const?對象。const修飾詞的作用是幫助程序員避免不必要的錯誤。程序員在無意中修改了本來不該被修改的變量的值,特別是對全局變量的無意修改。而在匯編語言中并沒有類似const這樣的修飾詞,因此匯編語言對程序員的要求更高。
練習
1、?編寫一段程序,從鍵盤讀取兩個整數,計算并顯示兩個整數的乘積。
2、?編寫一段程序,從鍵盤讀取三個整數,計算并顯示三個整數的和。