跳轉到內容

嵌入式系統/混合 C 和彙編程式設計

來自 Wikibooks,開放世界中的開放書籍

C 和彙編

[編輯 | 編輯原始碼]

許多程式設計師更習慣於用 C 語言編寫程式碼,這有充分的理由:C 語言是一種中級語言(與組合語言相比,組合語言是一種低階語言),它為程式設計師省去了實際實現的一些細節。

然而,有一些低階任務要麼可以用匯編語言更好地實現,要麼只能用匯編語言實現。此外,程式設計師經常需要檢視 C 編譯器的彙編輸出,並手動編輯或手動最佳化彙編程式碼,以實現編譯器無法實現的方式。組合語言對於時間關鍵型或即時程序也很有用,因為與高階語言不同,組合語言沒有關於程式碼如何編譯的歧義。時間可以嚴格控制,這對於編寫簡單的裝置驅動程式很有用。本節將介紹在混合 C 和彙編程式開發中使用的多種技術。

內聯彙編

[編輯 | 編輯原始碼]

在 C 程式設計專案中使用匯編程式碼片段最常見的方法之一是使用稱為**內聯彙編**的技術。在不同的編譯器中,內聯彙編的呼叫方式不同。此外,內聯彙編中使用的組合語言語法完全取決於 C 編譯器使用的彙編引擎。例如,Microsoft C++ 僅接受 MASM 語法中的內聯彙編命令,而 GNU GCC 僅接受 GAS 語法中的內聯彙編(也稱為 AT&T 語法)。本頁將討論一些常見編譯器中混合語言程式設計的基礎知識。

Microsoft C 編譯器

[編輯 | 編輯原始碼]
#include<stdio.h>
 
void main() {
 
   int a = 3, b = 3, c;
 
   asm {
      mov ax,a
      mov bx,b
      add ax,bx
      mov c,ax
   }
 
   printf("%d", c);
}
#include "stdio.h"
#include<iostream>

int main(void)
{
    unsigned char r1=0x90,r2=0x1A,r3=0x2C;
    unsigned short m1,m2,m3,m4;
    __asm__
    {
        MOV AL,r1;
        MOV AH,r2;
        MOV m1,AX;
        MOV BL,r1;
        ADD BL,3;
        MOV BH,r3;
        MOV m2,BX;
        INC BX;
        DEC BX:
        MOV m3,BX;
        SUB BX,AX;
        MOV m4,AX;
        
    }
    printf("r1=0x%x,r2=0x%x,r3=0x%x\n",r1,r2,r3);
    printf("m1=0x%x,m2=0x%x,m3=0x%x,m4=0x%x\n",m1,m2,m3,m4);
    return 0;
}

Borland C 編譯器

[編輯 | 編輯原始碼]

連結彙編

[編輯 | 編輯原始碼]

當彙編原始檔被彙編器彙編,C 原始檔被 C 編譯器編譯時,這兩個**目標檔案**可以由**連結器**連結在一起,形成最終的可執行檔案。這種方法的優點是彙編檔案可以使用程式設計師熟悉的任何語法和彙編器編寫。此外,如果需要更改彙編程式碼,所有這些程式碼都存在於一個單獨的檔案中,程式設計師可以輕鬆訪問。以這種方式混合彙編和 C 的唯一缺點是:a) 彙編器和編譯器都需要執行,b) 這些檔案需要由程式設計師手動連結在一起。這些額外的步驟相對容易,儘管這意味著程式設計師需要學習編譯器、彙編器和連結器的命令列語法。

內聯彙編與連結彙編

[編輯 | 編輯原始碼]

內聯彙編的優點

短的彙編例程可以直接嵌入到 C 程式碼檔案中的 C 函式中。然後,混合語言檔案可以用單個命令完全編譯到 C 編譯器(而不是用匯編器編譯彙編程式碼,用 C 編譯器編譯 C 程式碼,然後將它們連結在一起)。這種方法快速且簡單。如果內聯彙編嵌入到函式中,那麼程式設計師無需擔心#呼叫約定,即使將編譯器開關更改為不同的呼叫約定也是如此。

連結彙編的優點

如果選擇了新的微處理器,所有彙編命令都隔離在一個 ".asm" 檔案中。程式設計師只需更新該檔案 - 無需更改任何 ".c" 檔案(如果它們是可移植編寫的)。

呼叫約定

[編輯 | 編輯原始碼]

在編寫單獨的 C 和彙編模組並使用連結器將它們連結在一起時,重要的是要記住,許多高階 C 結構是明確定義的,需要由程式的彙編部分正確處理。也許混合語言程式設計中最大的障礙是函式呼叫約定的問題。所有 C 函式都是根據程式設計師選擇的特定約定實現的(如果你從未“選擇”過特定的呼叫約定,那是因為你的編譯器有一個預設設定)。本頁將介紹程式設計師可能會遇到的一些常見呼叫約定,並說明如何在組合語言中實現這些約定。

用一個編譯器編譯的程式碼與用不同的呼叫約定編譯的程式碼連結在一起時,將無法正常工作。如果程式碼是用 C 或其他高階語言(或嵌入到 C 函式中的內聯組合語言)編寫的,這只是一個小麻煩 - 程式設計師需要選擇她今天想使用哪個編譯器/最佳化開關,並以這種方式重新編譯程式的每個部分。將組合語言程式碼轉換為使用不同的呼叫約定需要更多的手動工作,並且更容易出現錯誤。

不幸的是,呼叫約定通常因編譯器而異 - 即使在同一個 CPU 上也是如此。偶爾,呼叫約定會從一個編譯器版本更改為下一個版本,甚至在同一個編譯器中,使用不同的“最佳化”開關時也會發生更改。

不幸的是,很多時候,特定編譯器版本的特定呼叫約定沒有充分的文件記錄。因此,組合語言程式設計師被迫使用逆向工程技術來確定他們需要知道的準確細節,以便呼叫用 C 語言編寫的函式,並以便接受來自用 C 語言編寫的函式的呼叫。

典型流程是:[1]

  • 用“.c”檔案編寫存根... 細節??? ... ... 你的組合語言函式所需的輸入和輸出數量和型別完全相同。
  • 使用適當的開關編譯該檔案,以生成包含 C 語言註釋的混合組合語言檔案(通常為“.cod”檔案)。(如果你的編譯器不能生成組合語言檔案,則可以手動反彙編二進位制“.obj”機器碼檔案,這很繁瑣)。
  • 將“.cod”檔案複製到“.asm”檔案。(有時你需要刪除編譯的十六進位制數字並註釋掉其他行,以便將其轉換為彙編器可以處理的內容)。
  • 測試呼叫約定 - 將“.asm”檔案編譯為“.obj”檔案,並將它(而不是存根“.c”檔案)連結到程式的其餘部分。測試以檢視“呼叫”是否正常工作。
  • 填寫您的“.asm”檔案 -“.asm”檔案現在應該在每個函式上包含適當的頁首和頁尾,以正確實現呼叫約定。註釋掉函式中間的存根程式碼,並使用您的組合語言實現填寫函式。
  • 測試。通常,程式設計師會逐條執行新程式碼中的每條指令,確保它按他們想要的方式執行。

引數傳遞

[編輯 | 編輯原始碼]
通常,引數在函式之間傳遞(無論是用 C 還是組合語言編寫),透過堆疊。例如,如果一個函式 foo1() 呼叫一個函式 foo2() 並帶有 2 個引數(例如字元 x 和 y),那麼在控制跳轉到 foo2() 的開頭之前,兩個位元組(大多數系統中字元的正常大小)將被填充為需要傳遞的值。一旦控制跳轉到新函式 foo2(),並且您在函式中使用這些值(作為引數傳遞),它們將從堆疊中檢索並使用。

有兩種引數傳遞技術正在使用,

1. 按值傳遞
2. 按引用傳遞

引數傳遞技術也可以使用

從右到左(C 風格)
從左到右(Pascal 風格)

在具有大量暫存器(如 ARM 和 Sparc)的處理器上,標準呼叫約定將 *所有* 引數(甚至返回值地址)都放在暫存器中。

在暫存器數量不足的處理器上(如 80x86 和 M8C),所有呼叫約定都必須至少將一些引數放在堆疊上或 RAM 中的其它地方。

一些呼叫約定允許“可重入程式碼”。

按值傳遞

[編輯 | 編輯原始碼]

使用按值傳遞,將實際值(文字內容)的副本傳遞。例如,如果您有一個接受兩個字元的函式,如下所示

void foo(char x, char y){
    x = x + 1;
    y = y + 2;
    putchar(x);
    putchar(y);
}

並且您如下呼叫此函式

char a,b;
a='A';
b='B';
foo(a,b);

那麼程式在呼叫函式 foo 之前,將 'A' 和 'B' 的 ASCII 值副本(分別為 65 和 66)壓入堆疊。您可以看到函式 foo() 中沒有提及變數 'a' 或 'b'。因此,您對 foo 中這兩個值所做的任何更改都不會影響呼叫函式中 a 和 b 的值。

按引用傳遞

[編輯 | 編輯原始碼]
想象一下,您需要將大量資料傳遞給一個函式,並在該函式中應用修改,將這些修改應用於原始變數的情況。這種情況的例子可能是一個將包含小寫字母的字串轉換為大寫的函式。將整個字串(特別是如果它很大)傳遞給函式,然後在轉換完成後將整個結果傳遞迴呼叫函式,這是一個不明智的決定。在這裡,我們將變數的地址傳遞給函式。這有兩個優點,一是您不必傳遞大量資料,從而節省了執行時間,二是您可以立即處理資料,以便在函式結束時,呼叫函式中的資料已修改。
但是請記住,您對按引用傳遞的變數所做的任何更改都會導致原始變數被修改。如果這不是您想要的,那麼您必須在呼叫函式之前手動複製變數。

80x86 / Pentium

[編輯 | 編輯原始碼]

在當前一代的 32 位和 64 位處理器出現之前,80x86 架構使用了一個複雜的段式記憶體模型(也稱為真實模式)。對於大多數目的,除非編寫極其底層的程式碼來直接與硬體或外設晶片介面,否則不會遇到這種情況。

現代程式碼通常被編寫為支援“保護模式”,在這種模式下,記憶體空間對於大多數目的可以被認為是扁平的。

以下關於保護模式中呼叫約定的資訊。

在 CDECL 呼叫約定中,以下內容成立

  • 引數以從右到左的順序傳遞到堆疊上,返回值傳遞到 eax 中。
  • 呼叫函式清理堆疊。這允許 CDECL 函式具有可變長度引數列表(也稱為變引數函式)。出於這個原因,編譯器不會將引數數量附加到函式名稱,因此彙編器和連結器無法確定是否使用了錯誤數量的引數。

變引數函式通常具有特殊的入口程式碼,由 va_start()、va_arg() C 偽函式生成。

考慮以下 C 指令

_cdecl int MyFunction1(int a, int b)
{
  return a + b;
}

以及以下函式呼叫

 x = MyFunction1(2, 3);

它們將分別生成以下彙編列表

:_MyFunction1
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov edx, [ebp + 12]
add eax, edx
pop ebp
ret

push 3
push 2
call _MyFunction1
add esp, 8

當轉換為彙編程式碼時,CDECL 函式幾乎總是以一個下劃線開頭(這就是為什麼所有以前的示例在彙編程式碼中都使用了“_”)。

STDCALL,也稱為“WINAPI”(以及一些其他名稱,具體取決於您閱讀的位置),幾乎被微軟專門用作 Win32 API 的標準呼叫約定。由於 STDCALL 是由微軟嚴格定義的,因此所有實現它的編譯器都以相同的方式實現它。

  • STDCALL 將引數從右到左傳遞,並將返回值傳遞到 eax 中。(微軟文件錯誤地聲稱引數是從左到右傳遞的,但事實並非如此。)
  • 被呼叫函式清理堆疊,與 CDECL 不同。這意味著 STDCALL 不允許可變長度引數列表。

考慮以下 C 函式

_stdcall int MyFunction2(int a, int b)
{
   return a + b;
}

以及呼叫指令

 x = MyFunction2(2, 3);

它們將分別生成以下彙編程式碼片段

:_MyFunction@8
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov edx, [ebp + 12]
add eax, edx
pop ebp
ret 8

push 3
push 2
call _MyFunction@8

這裡有幾個重要的事項需要說明

  1. 在函式體中,ret 指令有一個(可選)引數,指示函式返回時從堆疊中彈出多少個位元組。
  2. STDCALL 函式以一個下劃線開頭進行名稱修飾,後面跟著一個 @,然後是傳遞到堆疊上的引數數量(以位元組為單位)。這個數字在 32 位對齊的機器上始終是 4 的倍數。

FASTCALL 呼叫約定在所有編譯器之間並不完全標準,因此應謹慎使用。在 FASTCALL 中,前 2 或 3 個 32 位(或更小)引數傳遞到暫存器中,最常用的暫存器是 edx、eax 和 ecx。附加引數,或大於 4 位元組的引數,傳遞到堆疊上,通常以從右到左的順序(類似於 CDECL)。呼叫函式最常見的是負責清理堆疊,如果需要的話。

由於存在歧義,建議僅在速度至關重要的 1、2 或 3 個 32 位引數的情況下使用 FASTCALL。

以下 C 函式

_fastcall int MyFunction3(int a, int b)
{
   return a + b;
}

以及以下 C 函式呼叫

x = MyFunction3(2, 3);

將分別為被呼叫函式和呼叫函式生成以下彙編程式碼片段

:@MyFunction3@8
push ebp
mov ebp, esp ;many compilers create a stack frame even if it isn't used
add eax, edx ;a is in eax, b is in edx
pop ebp
ret

;the calling function
mov eax, 2
mov edx, 3
call @MyFunction3@8

FASTCALL 的名稱修飾在函式名稱前加一個 @,並在函式名稱後加 @x,其中 x 是傳遞給函式的引數數量(以位元組為單位)。

許多編譯器仍然為 FASTCALL 函式生成堆疊幀,特別是在 FASTCALL 函式本身呼叫另一個子例程的情況下。但是,如果 FASTCALL 函式不需要堆疊幀,那麼最佳化編譯器可以隨意省略它。

實際上所有使用 ARM 處理器的人都使用標準呼叫約定。與其他處理器相比,這使得混合 C 和 ARM 彙編程式設計相當容易。Thumb 函式最簡單的入口和出口序列為:[2]

an_example_subroutine:
    PUSH {save-registers, lr} ; one-line entry sequence
    ; ... first part of function ...
    BL thumb_sub 	;Must be in a space of +/- 4 MB 
    ; ... rest of function goes here, perhaps including other function calls
    ; somehow get the return value in a1 (r0) before returning
    POP {save-registers, pc} ; one-line return sequence

C 編譯器使用哪些暫存器?

資料型別

[編輯 | 編輯原始碼]

char 為 8 位,int 為 16 位,long 為 32 位,long long 為 64 位,float 和 double 為 32 位(這是唯一支援的浮點格式),指標為 16 位(函式指標為字地址,允許定址 ATmega 裝置上的整個 128K 程式記憶體空間,這些裝置具有 > 64 KB 的 flash ROM)。有一個 -mint8 選項(參見 C 編譯器 avr-gcc 的選項)可以使 int 為 8 位,但這不受 avr-libc 支援,並且違反了 C 標準(int 必須至少為 16 位)。它可能會在將來的版本中被刪除。

呼叫使用的暫存器 (r18-r27, r30-r31)

[編輯 | 編輯原始碼]

可能由 GNU GCC 分配給區域性資料。你可以在彙編子程式中自由使用它們。呼叫 C 子程式可能會覆蓋它們,呼叫者負責儲存和恢復它們。

呼叫儲存暫存器 (r2-r17, r28-r29)

[編輯 | 編輯原始碼]

可能由 GNU GCC 分配給區域性資料。呼叫 C 子程式不會改變它們。彙編子程式負責儲存和恢復這些暫存器(如果改變)。r29:r28(Y 指標)用作幀指標(指向棧上的區域性資料)(如果需要)。即使在編譯器為引數傳遞分配這些暫存器的情況下,被呼叫方也要儲存/保護這些暫存器內容的要求仍然適用。

固定暫存器 (r0, r1)

[編輯 | 編輯原始碼]

從不分配給 GNU GCC 用於區域性資料,但通常用於固定目的

r0 - 臨時暫存器,可以被任何 C 程式碼覆蓋(除了儲存它的中斷處理程式),可以用來在一部分彙編程式碼中暫時記住一些東西

r1 - 在任何 C 程式碼中假設始終為零,可以用來在一部分彙編程式碼中暫時記住一些東西,但使用後必須清除(clr r1)。這包括任何使用 [f]mul[s[u]] 指令,這些指令在 r1:r0 中返回結果。中斷處理程式在進入時儲存並清除 r1,在退出時恢復 r1(如果它不為零)。

函式呼叫約定

[編輯 | 編輯原始碼]

引數 - 從左到右分配,r25 到 r8。所有引數都對齊以從偶數編號的暫存器開始(奇數大小的引數,包括 char,在它們上面有一個空暫存器)。這允許在增強核心上更好地利用 movw 指令。

如果太多,則那些不適合的將在棧上傳遞。

返回值:8 位在 r24(不是 r25!)中,16 位在 r25:r24 中,最多 32 位在 r22-r25 中,最多 64 位在 r18-r25 中。8 位返回值由呼叫方零擴充套件/符號擴充套件到 16 位(無符號 char 比有符號 char 更有效 - 只需 clr r25)。具有可變引數列表的函式(printf 等)的引數都在棧上傳遞,char 擴充套件為 int。

警告:在 2000-07-01 之前沒有這種對齊,包括 gcc-2.95.2 的舊補丁。檢查你的舊彙編子程式,並相應地調整它們。

Microchip PIC

[編輯 | 編輯原始碼]

不幸的是,在為 Microchip PIC 編寫程式時,使用了多種(不相容)呼叫約定

並且 PIC 架構的幾個“特性”使得大多數子程式呼叫需要幾個指令——比許多其他處理器上的單個指令更冗長。

呼叫約定必須處理

  • “分頁”快閃記憶體程式儲存器架構
  • 硬體棧的限制(可能透過在軟體中模擬棧)
  • “分頁”RAM 資料儲存器架構
  • 確保中斷例程的子程式呼叫不會混淆中斷返回到主迴圈後所需的資訊。

Sparc 有特殊的硬體支援一個很好的呼叫約定

一個“暫存器視窗”...

  1. ARM 技術支援知識文章:[infocenter.arm.com/help/topic/com.arm.doc.faqs/ka8926.html "從 C 調用匯編例程"]
  2. ARM。 ARM 軟體開發工具包。 1997。第 9 章:ARM 過程呼叫標準。第 10 章:Thumb 過程呼叫標準。

進一步閱讀

[編輯 | 編輯原始碼]
[編輯 | 編輯原始碼]
華夏公益教科書