日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

内联汇编用法

發布時間:2023/12/10 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 内联汇编用法 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在 Linux 代碼中,經常可以看到在 C 代碼中,嵌入部分匯編代碼,這些代碼要么是與硬件體系相關的,要么是對性能有關鍵影響的。

在很久以前,我特別懼怕內嵌匯編代碼,直到后來把匯編部分的短板補上之后,才徹底終結這種心理。

也許你在工作中,幾乎不會涉及到內嵌匯編代碼的工作,但是一旦進入到系統的底層,或者需要對時間關鍵場景進行優化,這個時候你的知識儲備就發揮重要作用了!

這篇文章,我們就來詳細聊一聊在 C 語言中,如何通過 asm 關鍵字來嵌入匯編語言代碼,文中的 8 個示例代碼從簡單到復雜,逐步深入地介紹內聯匯編的關鍵語法規則。

希望這篇文章能夠成為你進階高手路上的墊腳石!

PS:

  • 示例代碼中使用的是 Linux 系統中 AT&T 匯編語法;

  • 文章中的 8 個示例代碼,可以在公眾號后臺回復關鍵字【?內聯匯編示范代碼?】,即可收到下載地址;

  • 一、基本 asm 格式

    gcc 編譯器支持 2 種形式的內聯 asm 代碼:

  • 基本 asm 格式:不支持操作數;

  • 擴展 asm 格式:支持操作數;

  • 1. 語法規則

    asm?[volatile]?("匯編指令")
  • 所有指令,必須用雙引號包裹起來;

  • 超過一條指令,必須用\n分隔符進行分割,為了排版,一般會加上\t;

  • 多條匯編指令,可以寫在一行,也可以寫在多行;

  • 關鍵字 asm 可以使用?asm?來替換;

  • volatile 是可選的,編譯器有可能對匯編代碼進行優化,使用 volatile 關鍵字之后,告訴編譯器不要優化手寫的內聯匯編代碼。

  • 2. test1.c 插入空指令

    #include?<stdio.h> int?main() {asm?("nop");printf("hello\n");asm?("nop\n\tnop\n\t""nop");return?0; }

    注意:C語言中會自動把兩個連續的字符串字面量拼接成一個,所以"nop\n\tnop\n\t" "nop"這兩個字符串會自動拼接成一個字符串。

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test1.s?test1.c

    test1.s 中內容如下(只貼出了內聯匯編代碼相關部分的代碼):

    #APP #?5?"test1.c"?1 nop #?0?""?2 #NO_APP //?這里是?printf?語句生成的代碼。 #APP #?7?"test1.c"?1 nop nop nop #?0?""?2 #NO_APP

    可以看到,內聯匯編代碼被兩個注釋(#APP ... #NO_APP)包裹起來。在源碼中嵌入了兩個匯編代碼,因此可以看到 gcc 編譯器生成的匯編代碼中包含了這兩部分代碼。

    這 2 部分嵌入的匯編代碼都是空指令 nop,沒有什么意義。

    3. test2.c 操作全局變量

    在 C 代碼中嵌入匯編指令,目的是用來計算,或者執行一定的功能,下面我們就來看一下,如何在內聯匯編指令中,操作全局變量。

    #include?<stdio.h>int?a?=?1; int?b?=?2; int?c;int?main() {asm?volatile?("movl?a,?%eax\n\t""addl?b,?%eax\n\t""movl?%eax,?c");printf("c?=?%d?\n",?c);return?0; }

    關于匯編指令中編譯器的基本知識:

    eax, ebx 都是 x86 平臺中的寄存器(32位),在基本asm格式中,寄存器的前面必須加上百分號%。

    32 位的寄存器 eax 可以當做 16 位來使用(ax),或者當做 8 位來使用(ah, al),本文只會按照 32 位來使用。

    代碼說明:

    movl a, %eax  // 把變量a的值復制到 %eax 寄存器中;

    addl b, %eax ?// 把變量 b 的值 與 %eax 寄存器中的值(a)相加,結果放在 %eax 寄存器中;

    movl %eax, c ?// 把 %eax 寄存器中的值復制到變量 c 中;

    img

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test2.s?test2.c

    test2.s 內容如下(只貼出與內聯匯編代碼相關部分):

    #APP #?9?"test2.c"?1 movl?a,?%eax addl?b,?%eax movl?%eax,?c #?0?""?2 #NO_APP

    可以看到,在內聯匯編代碼中,可以直接使用全局變量 a, b 的名稱來操作。執行 test2,可以得到正確的結果。

    思考一個問題:為什么在匯編代碼中,可以使用變量a, b, c?

    查看 test2.s 中內聯匯編代碼之前的部分,可以看到:

    .file"test2.c" .globla .data .align?4 .typea,?@object .sizea,?4 a: .long1 .globlb .align?4 .typeb,?@object .sizeb,?4 b: .long2 .commc,4,4

    變量 a, b 被 .globl 修飾,c 被 .comm 修飾,相當于是把它們導出為全局的,所以可以在匯編代碼中使用。

    那么問題來了:如果是一個局部變量,在匯編代代碼中就不會用 .globl 導出,此時在內聯匯編指令中,還可以直接使用嗎?

    眼見為實,我們把這 3 個變量放到 main 函數的內部,作為局部變量來試一下。

    4. test3.c 嘗試操作局部變量

    #include?<stdio.h> int?main() {????int?a?=?1;????int?b?=?2;????int?c;????asm("movl?a,?%eax\n\t"????????"addl?b,?%eax\n\t"????????"movl?%eax,?c");????printf("c?=?%d?\n",?c);????return?0; }

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test3.s?test3.c

    在 test3.s 中可以看到沒有 a, b, c 的導出符號,a 和 b 沒有其他地方使用,因此直接把他們的數值復制到棧空間中了:

    movl$1,?-20(%ebp)movl$2,?-16(%ebp)

    img

    我們來嘗試編譯成可執行程序:

    $?gcc?-m32?-o?test3?test3.c/tmp/ccuY0TOB.o:?In?function?`main':test3.c:(.text+0x20):?undefined?reference?to?`a'test3.c:(.text+0x26):?undefined?reference?to?`b'test3.c:(.text+0x2b):?undefined?reference?to?`c'collect2:?error:?ld?returned?1?exit?status

    編譯報錯:找不到對 a,b,c 的引用!那該怎么辦,才能使用局部變量呢?擴展 asm 格式!

    二、擴展 asm 格式

    1. 指令格式

    asm [volatile] ("匯編指令" : "輸出操作數列表" : "輸入操作數列表" : "改動的寄存器")

    格式說明

  • 匯編指令:與基本asm格式相同;

  • 輸出操作數列表:匯編代碼如何把處理結果傳遞到 C 代碼中;

  • 輸入操作數列表:C 代碼如何把數據傳遞給內聯匯編代碼;

  • 改動的寄存器:告訴編譯器,在內聯匯編代碼中,我們使用了哪些寄存器;

  • “改動的寄存器”可以省略,此時最后一個冒號可以不要,但是前面的冒號必須保留,即使輸出/輸入操作數列表為空。

  • 關于“改動的寄存器”再解釋一下:gcc 在編譯 C 代碼的時候,需要使用一系列寄存器;我們手寫的內聯匯編代碼中,也使用了一些寄存器。

    為了通知編譯器,讓它知道: 在內聯匯編代碼中有哪些寄存器被我們用戶使用了,可以在這里列舉出來,這樣的話,gcc 就會避免使用這些列舉出的寄存器

    2. 輸出和輸入操作數列表的格式

    在系統中,存儲變量的地方就2個:寄存器和內存。因此,告訴內聯匯編代碼輸出和輸入操作數,其實就是告訴它:

  • 向哪些寄存器或內存地址輸出結果;

  • 從哪些寄存器或內存地址讀取輸入數據;

  • 這個過程也要滿足一定的格式:

    "[輸出修飾符]約束"(寄存器或內存地址)

    (1)約束

    就是通過不同的字符,來告訴編譯器使用哪些寄存器,或者內存地址。包括下面這些字符:

    a: 使用 eax/ax/al 寄存器;

    b: 使用 ebx/bx/bl 寄存器;

    c: 使用 ecx/cx/cl 寄存器;

    d: 使用 edx/dx/dl 寄存器;

    r: 使用任何可用的通用寄存器;

    m: 使用變量的內存位置;

    先記住這幾個就夠用了,其他的約束選項還有:D, S, q, A, f, t, u等等,需要的時候再查看文檔。

    (2)輸出修飾符

    顧名思義,它使用來修飾輸出的,對輸出寄存器或內存地址提供額外的說明,包括下面4個修飾符:

  • +:被修飾的操作數可以讀取,可以寫入;

  • =:被修飾的操作數只能寫入;

  • %:被修飾的操作數可以和下一個操作數互換;

  • &:在內聯函數完成之前,可以刪除或者重新使用被修飾的操作數;

  • 語言描述比較抽象,直接看例子!

    3. test4.c 通過寄存器操作局部變量

    #include?<stdio.h> int?main() {????int?data1?=?1;????int?data2?=?2;????int?data3;????asm("movl?%%ebx,?%%eax\n\t"????????"addl?%%ecx,?%%eax"?:?"=a"(data3)?:?"b"(data1),"c"(data2));????printf("data3?=?%d?\n",?data3);????return?0; }

    有 2 個地方需要注意一下啊:

  • 在內聯匯編代碼中,沒有聲明“改動的寄存器”列表,也就是說可以省略掉(前面的冒號也不需要);

  • 擴展asm格式中,寄存器前面必須寫 2 個%;

  • 代碼解釋:

  • "b"(data1),"c"(data2) ==> 把變量 data1 復制到寄存器 %ebx,變量 data2 復制到寄存器 %ecx。這樣,內聯匯編代碼中,就可以通過這兩個寄存器來操作這兩個數了;

  • "=a"(data3) ==> 把處理結果放在寄存器 %eax 中,然后復制給變量data3。前面的修飾符等號意思是:會寫入往 %eax 中寫入數據,不會從中讀取數據;

  • 通過上面的這種格式,內聯匯編代碼中,就可以使用指定的寄存器來操作局部變量了,稍后將會看到局部變量是如何從經過棧空間,復制到寄存器中的。

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test4.s?test4.c

    匯編代碼 test4.s 如下:

    movl$1,?-20(%ebp)movl$2,?-16(%ebp)movl-20(%ebp),?%eaxmovl-16(%ebp),?%edxmovl%eax,?%ebxmovl%edx,?%ecx#APP#?10?"test4.c"?1movl?%ebx,?%eaxaddl?%ecx,?%eax#?0?""?2#NO_APP????movl%eax,?-12(%ebp)

    img

    可以看到,在進入手寫的內聯匯編代碼之前:

  • 把數字 1 通過棧空間(-20(%ebp)),復制到寄存器 %eax,再復制到寄存器 %ebx;

  • 把數字 2 通過棧空間(-16(%ebp)),復制到寄存器 %edx,再復制到寄存器 %ecx;

  • 這 2 個操作正是對應了內聯匯編代碼中的“輸入操作數列表”部分:"b"(data1),"c"(data2)。

    在內聯匯編代碼之后(#NO_APP 之后),把 %eax 寄存器中的值復制到棧中的 -12(%ebp) 位置,這個位置正是局部變量 data3 所在的位置,這樣就完成了輸出操作。

    4. test5.c 聲明改動的寄存器

    在 test4.c 中,我們沒有聲明改動的寄存器,所以編譯器可以任意選擇使用哪些寄存器。從生成的匯編代碼 test4.s 中可以看到,gcc 使用了 %edx 寄存器。

    那么我們來測試一下:告訴 gcc 不要使用 %edx 寄存器。

    #include?<stdio.h> int?main() {????int?data1?=?1;????int?data2?=?2;????int?data3;????asm("movl?%%ebx,?%%eax\n\t"????????"addl?%%ecx,?%%eax"????????:?"=a"(data3)????????:?"b"(data1),"c"(data2)????????:?"%edx");????printf("data3?=?%d?\n",?data3);????return?0; }

    代碼中,asm 指令最后部分 "%edx" ,就是用來告訴 gcc 編譯器:在內聯匯編代碼中,我們會使用到 %edx 寄存器,你就不要用它了。

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test5.s?test5.c

    來看一下生成的匯編代碼 test5.s:

    ????movl$1,?-20(%ebp)movl$2,?-16(%ebp)movl-20(%ebp),?%eaxmovl-16(%ebp),?%ecxmovl%eax,?%ebx#APP#?10?"test5.c"?1movl?%ebx,?%eaxaddl?%ecx,?%eax#?0?""?2#NO_APPmovl%eax,?-12(%ebp)

    img

    可以看到,在內聯匯編代碼之前,gcc 沒有選擇使用寄存器 %edx。

    三、使用占位符來代替寄存器名稱

    在上面的示例中,只使用了 2 個寄存器來操作 2 個局部變量,如果操作數有很多,那么在內聯匯編代碼中去寫每個寄存器的名稱,就顯得很不方便。

    因此,擴展 asm 格式為我們提供了另一種偷懶的方法,來使用輸出和輸入操作數列表中的寄存器:占位符!

    占位符有點類似于批處理腳本中,利用 2...來引用輸入參數一樣,內聯匯編代碼中的占位符,從輸出操作數列表中的寄存器開始從 0 編號,一直編號到輸入操作數列表中的所有寄存器。

    還是看例子比較直接!

    1. test6.c 使用占位符代替寄存器

    #include?<stdio.h> int?main() {????int?data1?=?1;????int?data2?=?2;???int?data3;????asm("addl?%1,?%2\n\t"????????"movl?%2,?%0"????????:?"=r"(data3)????????:?"r"(data1),"r"(data2));????printf("data3?=?%d?\n",?data3);????return?0; }

    代碼說明:

  • 輸出操作數列表"=r"(data3):約束使用字符 r, 也就是說不指定寄存器,由編譯器來選擇使用哪個寄存器來存儲結果,最后復制到局部變量 data3中;

  • 輸入操作數列表"r"(data1),"r"(data2):約束字符r, 不指定寄存器,由編譯器來選擇使用哪 2 個寄存器來接收局部變量 data1 和 data2;

  • 輸出操作數列表中只需要一個寄存器,因此在內聯匯編代碼中的 %0 就代表這個寄存器(即:從 0 開始計數);

  • 輸入操作數列表中有 2 個寄存器,因此在內聯匯編代碼中的 %1 和 %2 就代表這 2 個寄存器(即:從輸出操作數列表的最后一個寄存器開始順序計數);

  • ?

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test6.s?test6.c

    匯編代碼如下 test6.s:

    movl$1,?-20(%ebp)movl$2,?-16(%ebp)movl-20(%ebp),?%eaxmovl-16(%ebp),?%edx#APP#?10?"test6.c"?1addl?%eax,?%edxmovl?%edx,?%eax#?0?""?2#NO_APPmovl%eax,?-12(%ebp)

    img

    可以看到,gcc 編譯器選擇了 %eax 來存儲局部變量 data1,%edx 來存儲局部變量 data2 ,然后操作結果也存儲在 %eax 寄存器中。

    是不是感覺這樣操作就方便多了?不用我們來指定使用哪些寄存器,直接交給編譯器來選擇。

    在內聯匯編代碼中,使用 %0、%1 、%2 這樣的占位符來使用寄存器。

    別急,如果您覺得使用編號還是麻煩,容易出錯,還有另一個更方便的操作:擴展 asm 格式還允許給這些占位符重命名,也就是給每一個寄存器起一個別名,然后在內聯匯編代碼中使用別名來操作寄存器。

    還是看代碼!

    2. test7.c 給寄存器起別名

    #include?<stdio.h> int?main() {int?data1?=?1;int?data2?=?2;int?data3;asm("addl?%[v1],?%[v2]\n\t""movl?%[v2],?%[v3]":?[v3]"=r"(data3):?[v1]"r"(data1),[v2]"r"(data2));printf("data3?=?%d?\n",?data3);return?0; }

    代碼說明:

  • 輸出操作數列表:給寄存器(gcc 編譯器選擇的)取了一個別名 v3;

  • 輸入操作數列表:給寄存器(gcc 編譯器選擇的)取了一個別名 v1 和 v2;

  • 起立別名之后,在內聯匯編代碼中就可以直接使用這些別名( %[v1], %[v2], ?%[v3])來操作數據了。

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test7.s?test7.c

    再來看一下生成的匯編代碼 test7.s:

    movl$1,?-20(%ebp) movl$2,?-16(%ebp) movl-20(%ebp),?%eax movl-16(%ebp),?%edx #APP #?10?"test7.c"?1 addl?%eax,?%edx movl?%edx,?%eax #?0?""?2 #NO_APP movl%eax,?-12(%ebp)

    這部分的匯編代碼與 test6.s 中完全一樣!

    四、使用內存位置

    在以上的示例中,輸出操作數列表和輸入操作數列表部分,使用的都是寄存器(約束字符:a, b, c, d, r等等)。

    我們可以指定使用哪個寄存器,也可以交給編譯器來選擇使用哪些寄存器,通過寄存器來操作數據,速度會更快一些。

    如果我們愿意的話,也可以直接使用變量的內存地址來操作變量,此時就需要使用約束字符 m。

    1. test8.c 使用內存地址來操作數據

    #include?<stdio.h> int?main() {int?data1?=?1;int?data2?=?2;int?data3;asm("movl?%1,?%%eax\n\t""addl?%2,?%%eax\n\t""movl?%%eax,?%0":?"=m"(data3):?"m"(data1),"m"(data2));printf("data3?=?%d?\n",?data3);return?0; }

    代碼說明:

  • 輸出操作數列表 "=m"(data3):直接使用變量 data3 的內存地址;

  • 輸入操作數列表 "m"(data1),"m"(data2):直接使用變量 data1, data2 的內存地址;

  • 在內聯匯編代碼中,因為需要進行相加計算,因此需要使用一個寄存器(%eax),計算這個環節是肯定需要寄存器的。

    在操作那些內存地址中的數據時,使用的仍然是按順序編號的占位符。

    生成匯編代碼指令:

    gcc?-m32?-S?-o?test8.s?test8.c

    生成的匯編代碼如下 test8.s:

    movl$1,?-24(%ebp) movl$2,?-20(%ebp) #APP #?10?"test8.c"?1 movl?-24(%ebp),?%eax addl?-20(%ebp),?%eax movl?%eax,?-16(%ebp) #?0?""?2 #NO_APP movl-16(%ebp),?%eax

    img

    可以看到:在進入內聯匯編代碼之前,把 data1 和 data2 的值放在了棧中,然后直接把棧中的數據與寄存器 %eax 進行操作,最后再把操作結果(%eax),復制到棧中 data3 的位置(-16(%ebp))。

    五、總結

    通過以上 8 個示例,我們把內聯匯編代碼中的關鍵語法規則進行了講解,有了這個基礎,就可以在內聯匯編代碼中編寫更加復雜的指令了。

    希望以上內容對您能有所幫助!謝謝!

    文章中的 8 個示例代碼,可以在 CPP 開發者 公眾號后臺回復關鍵字【?內聯匯編示范代碼?】,即可收到下載地址。

    總結

    以上是生活随笔為你收集整理的内联汇编用法的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。