寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。本人非計算機專業,可能對本教程涉及的事物沒有了解的足夠深入,如有錯誤,歡迎批評指正。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 (一)羽夏看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 個字節。
-
結構體在內存是連續存儲的
-
指針只是一個新的類型,像普通的變量一樣,所有的指針類型的寬度為四個字節,本質為無符號類型
-
宏定義本質是在編譯器進行編譯之前預處理器對代碼文件進行替換
-
編譯發現重復定義的問題時,而單獨編譯各模塊不會出錯,則很可能為重復包含導致的重定義。
-
如何解決重復包含問題? 條件編譯 ;前置聲明(如果一個類型在另一個頭文件的函數或者類型,而頭文件盡量不能重復包含,直接在此頭文件聲明一下就行);