(八)羽夏看C語言——C番外篇


寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。本人非計算機專業,可能對本教程涉及的事物沒有了解的足夠深入,如有錯誤,歡迎批評指正。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 (一)羽夏看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碎碎念

  1. 變量是什么?是裝數據的一個容器。變量類型來約束數據的寬度。

  2. 在傳參的時候,參數以堆棧的形式進行傳遞

  3. 緩沖區是干什么的?來存局部變量的

  4. 文字顯示其實就是查表,然后將它在屏幕上畫出來

  5. 常見的文字編碼:ASCII、GB2312、Unicode

  6. ">>"右移運算符對於有符號數使用sar(算數右移,二進制數據右移,左邊補符號位),無符號數為shr(邏輯右移,二進制數據右移,左邊補0)

  7. "&"和"&&","|"和"||"雖然計算結果是一樣的,但"&&"和"||"效率高,只要前面的滿足表達式一定成立/不成立條件,就不再進行。

  8. 多維數組和一維數組在內存布局沒有任何區別,都是線性存儲的,只是為了開發人員方便使用。比如定義一個 int a[3][3][4],如果我使用 a[1][2][3],相當於在一維數組 a[3*3*4]中查詢 a[1*3*4+2*4+3]。

  9. 提升的堆棧(緩沖區的大小)與聲明的變量所占的字節數有關,如果變量不聲明提升40個字節,如聲明1個int,則會提升40+4個字節。但是,如果聲明的變量不是本機寬度的正數倍,則按本機寬度的整數倍+1再乘以本機寬度處理。

    本機寬度是指在硬件層面最擅長處理數據位數,比如聲明一個char[10]的變量,在32位的系統下,本機寬度為4(64位的為8),由於10/4還有余數2故提升 40+4*(2+1)=52 個字節。

  10. 結構體在內存是連續存儲的

  11. 指針只是一個新的類型,像普通的變量一樣,所有的指針類型的寬度為四個字節,本質為無符號類型

  12. 宏定義本質是在編譯器進行編譯之前預處理器對代碼文件進行替換

  13. 編譯發現重復定義的問題時,而單獨編譯各模塊不會出錯,則很可能為重復包含導致的重定義。

  14. 如何解決重復包含問題? 條件編譯 ;前置聲明(如果一個類型在另一個頭文件的函數或者類型,而頭文件盡量不能重復包含,直接在此頭文件聲明一下就行);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM