C 語言編程 — 結構化程序流的匯編代碼與 CPU 指令集

      網友投稿 904 2025-04-01

      目錄

      文章目錄

      目錄

      文章目錄

      為什么要保留匯編語言

      順序程序流

      條件程序流

      循環程序流

      函數調用棧的工作原理

      文章目錄

      《C 語言編程 — GCC 工具鏈》

      《C 語言編程 — 程序的編譯流程》

      《C 語言編程 — 靜態庫、動態庫和共享庫》

      《C 語言編程 — 程序的裝載與運行》

      《計算機組成原理 — 指令系統》

      《C 語言編程 — 結構化程序流的匯編代碼與 CPU 指令集》

      為什么要保留匯編語言

      匯編語言是與機器語言最接近的高級編程語言(或稱為中級編程語言),匯編語言基本上與機器語言對應,即匯編指令和計算機指令是相對匹配的。雖然匯編語言具有與硬件的關系密切,占用內存小,運行速度快等優點,但也具有可讀性低、可重用性差,開發效率低下等問題。高級語言的出現是為了解決這些問題,讓軟件開發變得更加簡單高效,易于協作。但高級語言也存在自己的缺陷,例如:難以編寫直接操作硬件設備的程序等。

      所以為了權衡上述的問題,最終匯編語言被作為中間的狀態保留了下來。一些高級語言(e.g. C 語言)提供了與匯編語言之間的調用接口,匯編程序可作為高級語言的外部過程或函數,利用堆棧在兩者之間傳遞參數或參數的訪問地址。兩者的源程序通過編譯或匯編生成目標文件(OBJ)之后再利用連接程序(linker)把它們連接成為可執行文件便可在計算機上運行了。保留匯編語言還為程序員提供一種調優的手段,無論是 C 程序還是 Python 程序,當我們要進行代碼性能優化時,了解程序的匯編代碼是一個不錯的切入點。

      順序程序流

      計算機指令是一種邏輯上的抽象設計,而機器碼則是計算機指令的物理表現。機器碼(Machine Code),又稱為機器語言,本質是由 0 和 1 組成的數字序列。一條機器碼就是一條計算機指令。程序由指令組成,但讓人類使用機器碼來編寫程序顯然是不人道的,所以逐步發展了對人類更加友好的高級編程語言。這里我們需要了解計算機是如何將高級編程語言編譯為機器碼的。

      Step 1. 編寫高級語言程序。

      // test.c int main() { int a = 1; int b = 2; a = a + b; }

      1

      2

      3

      4

      5

      6

      7

      Step 2. 編譯(Compile),將高級語言編譯成匯編語言(ASM)程序。

      $ gcc -g -c test.c

      1

      Step 3. 使用 objdump 命令反匯編目標文件,輸出可閱讀的二進制信息。下述左側的一堆數字序列就是一條條機器碼,右側 push、mov、add、pop 一類的就是匯編代碼。

      $ objdump -d -M intel -S test.o test.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000

      : int main() { 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp int a = 1; 4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 int b = 2; b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 a = a + b; 12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 15: 01 45 fc add DWORD PTR [rbp-0x4],eax } 18: 5d pop rbp 19: c3 ret

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      NOTE:這里的程序入口是 main() 函數,而不是第 0 條匯編代碼。

      條件程序流

      值得注意的是,某些特殊的指令,比如跳轉指令,會主動修改 PC 的內容,此時下一條地址就不是從存儲器中順序加載的了,而是到特定的位置加載指令內容。這就是 if…else 條件語句,while/for 循環語句的底層支撐原理。

      Step 1. 編寫高級語言程序。

      // test.c #include #include int main() { srand(time(NULL)); int r = rand() % 2; int a = 10; if (r == 0) { a = 1; } else { a = 2; } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      Step 2. 編譯(Compile),將高級語言編譯成匯編語言。

      $ gcc -g -c test.c

      1

      Step 3. 使用 objdump 命令反匯編目標文件,輸出可閱讀的二進制信息。我們主要分析 if…else 語句。

      if (r == 0) 33: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0 37: 75 09 jne 42 { a = 1; 39: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1 40: eb 07 jmp 49 } else { a = 2; 42: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      首先進入條件判斷,匯編代碼為 cmp 比較指令,比較數 1:DWORD PTR [rbp-0x4] 表示變量 r 是一個 32 位整數,數據在寄存器 [rbp-0x4] 中;比較數 2:0x0 表示常量 0 的十六進制。比較的結果會存入到 條件碼寄存器,等待被其他指令讀取。當判斷條件為 True 時,ZF 設置為 1,反正設置為 0。

      條件碼寄存器(Condition Code)是一種單個位寄存器,它們的值只能為 0 或者 1。當有算術與邏輯操作發生時,這些條件碼寄存器當中的值就隨之發生變化。后續的指令通過檢測這些條件碼寄存器來執行條件分支指令。常用的條件碼類型如下:

      CF:進位標志寄存器。最近的操作是最高位產生了進位。它可以記錄無符號操作的溢出,當溢出時會被設為 1。

      ZF:零標志寄存器,最近的操作得出的結果為 0。當計算結果為 0 時將會被設為 1。

      SF:符號標志寄存器,最近的操作得到的結果為負數。當計算結果為負數時會被設為 1。

      OF:溢出標志寄存器,最近的操作導致一個補碼溢出(正溢出或負溢出)。當計算結果導致了補碼溢出時,會被設為 1。

      回到正題,PC 繼續自增,執行下一條 jnp 指令。jnp(jump if not equal)會查看 ZF 的內容,若為 0 則跳轉到地址 42 (42 表示匯編代碼的行號)。前文提到,當 CPU 執行跳轉類指令時,PC 就不再通過自增的方式來獲得下一條指令的地址,而是直接被設置了 42 行對應的地址。由此,CPU 會繼續將 42 對應的指令讀取到 IR 中并執行下去。

      42 行執行的是 mov 指令,表示將操作數 2:0x2 移入到 操作數 1:DWORD PTR [rbp-0x8] 中。就是一個賦值語句的底層實現支撐。接下來 PC 恢復如常,繼續以自增的方式獲取下一條指令的地址。

      循環程序流

      C 語言代碼

      // test.c int main() { int a = 0; int i; for (i = 0; i < 3; i++) { a += i; } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      計算機指令與匯編代碼

      for (i = 0; i < 3; i++) b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0 12: eb 0a jmp 1e { a += i; 14: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 17: 01 45 fc add DWORD PTR [rbp-0x4],eax for (i = 0; i < 3; i++) 1a: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x1 1e: 83 7d f8 02 cmp DWORD PTR [rbp-0x8],0x2 22: 7e f0 jle 14 }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      C 語言編程 — 結構化程序流的匯編代碼與 CPU 指令集

      11

      12

      13

      函數調用棧的工作原理

      與普通的跳轉程序(e.g. if…else、while/for)不同,函數調用的特點在于具有回歸(return)的特點,在調用的函數執行完之后會再次回到執行調用的 call 指令的位置,繼續往下執行。能夠實現這個效果,完全依賴堆棧(Stack)存儲區的特性。 首先我們需要了解幾個概念。

      堆棧(Stack):是有若干個連續的存儲器單元組成的先進后出(FILO)存儲區,用于提供操作數、保存運算結果、暫存中斷和子程序調用時的線程數據及返回地址。通過執行堆棧的 Push(壓棧)和 Pop(出棧)操作可以將指定的數據在堆棧中放入和取出。堆棧具有棧頂和棧底之分,棧頂的地址最低,而棧底的地址最高。堆棧的 FILO 的特性非常適用于函數調用的場景:父函數調用子函數,父函數在前,子函數在后;返回時,子函數先返回,父函數后返回。

      棧幀(Stack Frame):是堆棧中的邏輯空間,每次函數調用都會在堆棧中生成一個棧幀,對應著一個未運行完的函數。從邏輯上講,棧幀就是一個函數執行的環境,保存了函數的參數、函數的局部變量以及函數執行完后返回到哪里的返回地址等等。棧幀的本質是兩個指針寄存器: EBP(基址指針,又稱幀指針)和 ESP(棧指針)。其中 EBP 指向幀底,而 ESP 指向棧頂。當程序運行時,ESP 是可以移動的,大多數信息的訪問都通過移動 ESP 來完成,而 EBP 會一直處于幀低。EBP ~ ESP 之間的地址空間,就是當前執行函數的地址空間。

      NOTE:EBP 指向當前位于系統棧最上邊一個棧幀的底部,而不是指向系統棧的底部。嚴格說來,“棧幀底部” 和 “系統棧底部” 不是同一個概念,而 ESP 所指的棧幀頂部和系統棧頂部是同一個位置。

      簡單概括一下函數調用的堆棧行為,ESP 隨著當前函數的壓棧和出棧會不斷的移動,但由于 EBP 的存在,所以當前執行函數棧幀的邊界是始終清晰的。當一個當前的子函數調用完成之后,EBP 就會跳到父函數棧幀的底部,而 ESP 也會隨其自然的來到父函數棧幀的頭部。所以,理解函數調用堆棧的運作原理,主要要掌握 EBP 和 ESP 的動向。下面以一個例子來說明。

      NOTE:我們習慣將將父函數(調用函數的函數)稱為 “調用者(Caller)”,將子函數(被調用的函數)稱為 “被調用者(Callee)”。

      C 程序代碼

      #include int add(int a, int b) { int result = 0; result = a + b; return result; } int main(int argc, char *argv[]) { int result = 0; result = add(1, 2); printf("result = %d \r\n", result); return 0; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      使用gcc編譯,然后gdb反匯編main函數,看看它是如何調用add函數的

      (gdb) disassemble main Dump of assembler code for function main: 0x08048439 <+0>: push %ebp 0x0804843a <+1>: mov %esp,%ebp 0x0804843c <+3>: and $0xfffffff0,%esp 0x0804843f <+6>: sub $0x20,%esp 0x08048442 <+9>: movl $0x0,0x1c(%esp) # 給 result 變量賦 0 值 0x0804844a <+17>: movl $0x2,0x4(%esp) # 將第 2 個參數 argv 壓棧(該參數偏移為esp+0x04) 0x08048452 <+25>: movl $0x1,(%esp) # 將第 1 個參數 argc 壓棧(該參數偏移為esp+0x00) 0x08048459 <+32>: call 0x804841c # 調用 add 函數 0x0804845e <+37>: mov %eax,0x1c(%esp) # 將 add 函數的返回值地址賦給 result 變量,作為子函數調用完之后的回歸點 0x08048462 <+41>: mov 0x1c(%esp),%eax 0x08048466 <+45>: mov %eax,0x4(%esp) 0x0804846a <+49>: movl $0x8048510,(%esp) 0x08048471 <+56>: call 0x80482f0 0x08048476 <+61>: mov $0x0,%eax 0x0804847b <+66>: leave 0x0804847c <+67>: ret End of assembler dump. (gdb) disassemble add Dump of assembler code for function add: 0x0804841c <+0>: push %ebp # 將 ebp 壓棧(保存函數調用者的棧幀基址) 0x0804841d <+1>: mov %esp,%ebp # 將 ebp 指向棧頂 esp(設置當前被調用函數的棧幀基址) 0x0804841f <+3>: sub $0x10,%esp # 分配??臻g(棧向低地址方向生長) 0x08048422 <+6>: movl $0x0,-0x4(%ebp) # 給 result 變量賦 0 值(該變量偏移為ebp-0x04) 0x08048429 <+13>: mov 0xc(%ebp),%eax # 將第 2 個參數的值賦給 eax 寄存器(準備運算) 0x0804842c <+16>: mov 0x8(%ebp),%edx # 將第 1 個參數的值賦給 edx 寄存器(準備運算) 0x0804842f <+19>: add %edx,%eax # 運算器執行加法運算 (edx+eax),結果保存在 eax 寄存器中 0x08048431 <+21>: mov %eax,-0x4(%ebp) # 將運算結果 eax 賦給 result 變量 0x08048434 <+24>: mov -0x4(%ebp),%eax # 將 result 變量的值賦給 eax 寄存器(eax 的地址將作為函數返回值) 0x08048437 <+27>: leave # 恢復函數調用者的棧幀基址(pop %ebp) 0x08048438 <+28>: ret # 返回(準備執行下條指令) End of assembler dump.

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      示意圖

      可見,每一次函數調用,都會對調用者的棧幀基址 EBP 進行壓棧操作(為了調用回歸),并且由于子函數的棧幀基址 EBP 來自于棧指針 ESP 而來(生成新的子函數的棧幀),所以各層函數的棧幀基址很巧妙的構成了一個鏈,即當前的棧幀基址指向下一層函數棧幀基址所在的位置。

      由此當子函數執行完成時,ESP 依舊在棧頂,但 EBP 就跳轉到父函數的棧幀底部了,并且堆棧下一個彈出的就是子函數的調用回歸點,最終程序流回到調用點并繼續往下執行。

      通過函數調用堆棧的工作原理我們可以看出,無論程序中具有多少層的函數調用,或遞歸調用,只需要維護好每個棧幀的 EBP 和 ESP 就可以管理還函數之間的跳轉。但堆棧也是由容量限制的,如果函數調用的層級太多就會出現棧溢出的錯誤(Stack Overflow)。

      C 語言 單片機 匯編語言

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel中錄入乘號的方法
      下一篇:2010excel表格如何做成圖片(Excel表格制作圖片)
      相關文章
      色噜噜噜噜亚洲第一| 精品国产日韩亚洲一区在线| 亚洲精品乱码久久久久久蜜桃| 亚洲av无码无线在线观看| 亚洲日韩AV一区二区三区中文| 亚洲一区二区三区91| 亚洲国产片在线观看| 亚洲乱码在线视频| 亚洲一级毛片视频| 亚洲成_人网站图片| 日韩亚洲不卡在线视频中文字幕在线观看| 亚洲国产美女视频| 亚洲伊人久久大香线蕉在观| 亚洲性69影院在线观看| 亚洲一区在线视频观看| 亚洲一区二区三区免费视频| 亚洲jizzjizz在线播放久| 亚洲国产区男人本色在线观看| 亚洲人成网男女大片在线播放| 亚洲男人天堂2022| 亚洲精品无码久久久久APP| 日韩在线视精品在亚洲| 亚洲国产精品日韩| 国产亚洲精品看片在线观看| 亚洲人成人一区二区三区| 亚洲国产成人片在线观看| 亚洲欧洲在线观看| 亚洲欧洲精品久久| 亚洲日韩久久综合中文字幕| 亚洲AV色欲色欲WWW| 亚洲国产一区视频| 亚洲色婷婷一区二区三区| 亚洲AV无码成人精品区在线观看| 亚洲日本在线观看| 亚洲激情校园春色| 亚洲中文字幕无码久久| 国产亚洲精品国产福利在线观看| 亚洲免费在线观看| 亚洲gv猛男gv无码男同短文| 337p日本欧洲亚洲大胆艺术| 国产v亚洲v天堂a无|