寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。本人非計算機專業,可能對本教程涉及的事物沒有了解的足夠深入,如有錯誤,歡迎批評指正。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀(一)羽夏看C語言——簡述 ,方便學習本教程。本篇是C番外篇,會將零碎的東西重新集合起來介紹,可能會與前面有些重複或重合。
️ C語言和反彙編
C語言的入口main函式反彙編指令
int main()
{
return 0;
}
反彙編:
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,[ebp-0x40]
mov ecx,0x10
mov eax,0xcccccccc
rep stosd
xor eax,eax
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
️ 函式呼叫詳解
C語言
int Plus(int x,int y)
{
return x+y;
}
void main()
{
Plus(1,2);
}
反彙編:
/*main函式*/
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,[ebp-0x40]
mov ecx,0x10
mov eax,0xcccccccc
rep stosd
push 2 //壓入倒數第一個引數
push 1 //壓入倒數第二個引數
call 0x40100c //呼叫函式,假設Plus函式的地址為0x40100c
add esp,8 //儲存堆疊平衡,恢復引數佔用的堆疊
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
/*Plus函式:地址<0x40100c>*/
push ebp //將ebp的值壓入堆疊中
mov ebp,esp //將esp的值賦給ebp
sub esp,0x40 //提升堆疊,提供緩衝區
push ebx
push esi
push edi
/*===============================*/
lea edi,[ebp-0x40] //獲取esp-0x40處的值賦給edi,提供目標
mov ecx,0x10 //將0x10賦給ecx,提供計數
mov eax,0xcccccccc //將4個CC斷點賦給eax,提供資料來源
rep stosd //從edi處填充eax的資料ecx次,每次edi+8h
mov eax,dword ptr[ebp+8h] //eax=x
add eax,dword ptr[ebp+0xCh] //eax+=y
//eax作為函式的返回值
/*==========恢復下面的值==========*/
pop edi
pop esi
pop ebx
/*====下面的操作是恢復棧底棧頂====*/
mov esp,ebp
pop ebp
ret
️ 全域性變數
1、編譯的時候就已經確定了記憶體地址和寬度,變數名就是記憶體地址的別名。
2、如果不重寫編譯,全域性變數的記憶體地址不變。
️ 區域性變數
1、區域性變數是函式內部申請的,如果函式沒有執行,那麼區域性變數沒有記憶體空間。
2、區域性變數的記憶體是在堆疊中分配的,程式執行時才分配。我們無法預知程式何時執行,這也就意味著,我們無法確定區域性變數的記憶體地址。
3、因為區域性變數地址記憶體是不確定的,所以,區域性變數只能在函式內部使用,其他函式不能使用。
️ 堆疊圖
️ 資料型別
整型型別資料 | |||
---|---|---|---|
char | 8BIT | 1位元組 | 0~0xFF |
short | 16BIT | 2位元組 | 0~0xFFFF |
int | 32BIT | 4位元組 | 0~0xFFFFFFFF |
long | 32BIT | 4位元組 | 0~0xFFFFFFFF |
️ 有符號與無符號的區別:
<1>正數有符號數與無符號數無區別
<2>拓展時與比較時才有區別
浮點型別資料 | |
---|---|
float | 4位元組 |
double | 8位元組 |
long double | 8位元組(某些平臺的編譯器可能是16個位元組) |
float和double在儲存方式上都是遵從IEEE編碼規範的。對於整數部分,轉化方式遞迴取餘除以2,再逆序就是。而小數部分是遞迴乘二取整,正序就是。故用二進位制描述小數,不可能做到完全精確。
️ 將一個float型轉化為記憶體儲存格式的步驟為:
<1>先將這個實數的絕對值化為二進位制格式
<2>將這個二進位制格式實數的小數點左移或右移n位,直到小數點移動到第一個有效數字的右邊。
<3>從小數點右邊第一位開始數出二十三位數字放入第22到第0位。<4>如果實數是正的,則在第31位放入“0”,否則放入“1”。
<5> 如果n是左移得到的,說明指數是正的,第30位放入“1”。如果n是右移得到的或n=0,則第30位放入“0”。
<6> 如果n是左移得到的,則將n減去1後化為二進位制,並在左邊加“0”補足七位,放入第29到第23位。
<7> 如果n是右移得到的或n=0,則將n化為二進位制後在左邊加“0”補足七位,再各位求反,再放入第29到第23位。
️ 浮點型別的精度
float和double的精度是由尾數的位數來決定的:
- float:2^23= 8388608,一共7位,這意味著最多能有7位有效數字;
- double:2^52,一共16位,這意味著最多能有16位有效數字;
️ 當分支比較多的時候,switch為什麼效率比if-elif高:
switch語句
switch (x)
0xBF10F8 mov eax,dword ptr [x]
0xBF10FB mov dword ptr [ebp-0D0h],eax
0xBF1101 mov ecx,dword ptr [ebp-0D0h]
0xBF1107 sub ecx,1
0xBF110A mov dword ptr [ebp-0D0h],ecx
0xBF1110 cmp dword ptr [ebp-0D0h],4
0xBF1117 ja $LN8+0Fh (0BF1171h)
0xBF1119 mov edx,dword ptr [ebp-0D0h]
0xBF111F jmp dword ptr [edx*4+0BF11A4h]
{
case 1:
printf("1");
0xBF1126 push offset string "1" (0C711B0h)
0xBF112B call printf (0BF11C0h)
0xBF1130 add esp,4
break;
0xBF1133 jmp $LN8+1Ch (0BF117Eh)
case 2:
printf("2");
0xBF1135 push offset string "2" (0C711B4h)
0xBF113A call printf (0BF11C0h)
0xBF113F add esp,4
break;
0xBF1142 jmp $LN8+1Ch (0BF117Eh)
case 3:
printf("3");
0xBF1144 push offset string "3" (0C711B8h)
0xBF1149 call printf (0BF11C0h)
0xBF114E add esp,4
break;
0xBF1151 jmp $LN8+1Ch (0BF117Eh)
case 4:
printf("4");
0xBF1153 push offset string "4" (0C711BCh)
0xBF1158 call printf (0BF11C0h)
0xBF115D add esp,4
break;
0xBF1160 jmp $LN8+1Ch (0BF117Eh)
case 5:
printf("5");
0xBF1162 push offset string "5" (0C711C0h)
0xBF1167 call printf (0BF11C0h)
0xBF116C add esp,4
break;
0xBF116F jmp $LN8+1Ch (0BF117Eh)
default:
printf("-1");
0xBF1171 push offset string "-1" (0C711C4h)
0xBF1176 call printf (0BF11C0h)
0xBF117B add esp,4
break;
}
if-elif
if (x==1)
0x6810F8 cmp dword ptr [x],1
0x6810FC jne main+4Dh (068110Dh)
{
printf("1");
0x6810FE push offset string "1" (07011B0h)
0x681103 call printf (06811A0h)
0x681108 add esp,4
0x68110B jmp main+0AEh (068116Eh)
}else if (x==2)
0x68110D cmp dword ptr [x],2
0x681111 jne main+62h (0681122h)
{
printf("2");
0x681113 push offset string "2" (07011B4h)
0x681118 call printf (06811A0h)
0x68111D add esp,4
}
0x681120 jmp main+0AEh (068116Eh)
else if (x==3)
0x681122 cmp dword ptr [x],3
0x681126 jne main+77h (0681137h)
{
printf("3");
0x681128 push offset string "3" (07011B8h)
0x68112D call printf (06811A0h)
0x681132 add esp,4
}
0x681135 jmp main+0AEh (068116Eh)
else if (x==4)
0x681137 cmp dword ptr [x],4
0x68113B jne main+8Ch (068114Ch)
{
printf("4");
0x68113D push offset string "4" (07011BCh)
0x681142 call printf (06811A0h)
0x681147 add esp,4
}
0x68114A jmp main+0AEh (068116Eh)
else if (x==5)
0x68114C cmp dword ptr [x],5
0x681150 jne main+0A1h (0681161h)
{
printf("5");
0x681152 push offset string "5" (07011C0h)
0x681157 call printf (06811A0h)
0x68115C add esp,4
}
0x68115F jmp main+0AEh (068116Eh)
else
{
printf("-1");
0x681161 push offset string "-1" (07011C4h)
0x681166 call printf (06811A0h)
0x68116B add esp,4
}
由上可知,當條件比較多且比較有規律的時候,switch會生成裝有記憶體地址位置的序列表,通過計算直接跳轉到要去的位置,不需要多次判斷。
️ 位元組對齊
1、一個變數佔用n個位元組,則該變數的起始地址必須是n的整數倍,即:存放起始地址%n= 0。
2、如果是結構體,那麼結構體的起始地址是其最寬資料型別成員的整數倍。
️ 當對空間要求較高的時候,可以通過#pragma pack(n)來改變結構體成員的對齊方式
#pragma pack(1)
struct Test
{
char a;
int b;
};
#pragma pack()
1、#pragma pack(n)
中n用來設定變數以n位元組對齊方式,可以設定的值包括:1、2、4、8 ,VC編譯器預設是8。
2、結構體大總大小:N=Min(最大成員,對齊引數),是N的整數倍。
️ 指標型別的加減
1、不帶"*"型別的變數,"++"或者"- -"都是加1或者減1
2、帶"*"型別的變數,"++"或者"- -"新增(減少)的數量是去掉一個 * 後變數的寬度
3、指標型別的變數可以加、減一個整數,但不能乘或者除
4、指標型別變數與其他整數相加或者相減時:
指標型別變數 + N=指標型別變數 + N *(去掉一個 * 後型別的寬度)指標型別變數 - N=指標型別變數 - N *(去掉一個 * 後型別的寬度)
️ 取值: *()與[]可以相互轉換
*(p+i)= p[i]
*(*(p+i)+k)= p[i][k]
*(*(*(p+i)+k)+m)= p[i][k][m]
*(*(*(*(*(p+i)+k)+m)+w)+t)= p[i][k][m][w][t]
️ 常見的呼叫約定
呼叫約定 | 引數壓棧順序 | 平衡堆疊 |
---|---|---|
cdecl | 從右至左入棧 | 呼叫者清理棧 |
stdcall | 從右至左入棧 | 自身清理堆疊 |
fastcall | 從右至左入棧,ECX/EDX傳送前兩個,剩下的通過堆疊 | 自身清理堆疊 |
️ 常見的預編譯指令
指令 | 用途 |
---|---|
#define | 定義巨集 |
#undef | 取消已定義的巨集 |
#if | 如果給定條件為真,則編譯下面程式碼 |
#elif | 如果前面的lif給定條件不為真,當前條件為真,則編譯下面程式碼 |
#else | 同else |
#endif | 結束一個#if ......#else條件編譯塊 |
#ifdef | 如果巨集已經定義,則編譯下面程式碼 |
#ifndef | 如果巨集沒有定義,則編譯下面程式碼 |
#include | 包含檔案 |
️ C碎碎念
- 變數是什麼?是裝資料的一個容器。變數型別來約束資料的寬度。
- 在傳參的時候,引數以堆疊的形式進行傳遞
- 緩衝區是幹什麼的?來存區域性變數的
- 文字顯示其實就是查表,然後將它在螢幕上畫出來
- 常見的文字編碼:ASCII、GB2312、Unicode
- ">>"右移運算子對於有符號數使用sar(算數右移,二進位制資料右移,左邊補符號位),無符號數為shr(邏輯右移,二進位制資料右移,左邊補0)
- "&"和"&&","|"和"||"雖然計算結果是一樣的,但"&&"和"||"效率高,只要前面的滿足表示式一定成立/不成立條件,就不再進行。
- 多維陣列和一維陣列在記憶體佈局沒有任何區別,都是線性儲存的,只是為了開發人員方便使用。比如定義一個 int a[3][3][4],如果我使用 a[1][2][3],相當於在一維陣列 a[3*3*4]中查詢 a[1*3*4+2*4+3]。
- 提升的堆疊(緩衝區的大小)與宣告的變數所佔的位元組數有關,如果變數不宣告提升40個位元組,如宣告1個int,則會提升40+4個位元組。但是,如果宣告的變數不是本機寬度的正數倍,則按本機寬度的整數倍+1再乘以本機寬度處理。
本機寬度是指在硬體層面最擅長處理資料位數,比如宣告一個char[10]的變數,在32位的系統下,本機寬度為4(64位的為8),由於10/4還有餘數2故提升 40+4*(2+1)=52 個位元組。
- 結構體在記憶體是連續儲存的
- 指標只是一個新的型別,像普通的變數一樣,所有的指標型別的寬度為四個位元組,本質為無符號型別
- 巨集定義本質是在編譯器進行編譯之前前處理器對程式碼檔案進行替換
- 編譯發現重複定義的問題時,而單獨編譯各模組不會出錯,則很可能為重複包含導致的重定義。
- 如何解決重複包含問題? 條件編譯 ;前置宣告(如果一個型別在另一個頭檔案的函式或者型別,而標頭檔案儘量不能重複包含,直接在此標頭檔案宣告一下就行);