【原創】X86_64/X86 GNU彙編、寄存器、內嵌彙編

整理的X86_64/X86彙編、寄存器、C內嵌彙編筆記,主要用於查閱使用。

一、彙編語言

計算機的處理器有很多不同的架構,比如 x86-64、ARM、Power 等,每種處理器的指令集都不相同,那也就意味着彙編語言不同。目前的電腦,CPU 一般是 x86-64 架構,是 64 位機。

C語言代碼:

#include <stdio.h>
int main(int argc, char* argv[])
{
    printf("Hello %s!\n", "WSG");
    return 0;
}

編譯爲彙編:

gcc -S -O2 hello.c -o hello.s
或
clang -S -O2 hello.c -o hello.s

對應的彙編代碼如下:

.file   "hello.c"
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "WSG"
.LC1:
        .string "Hello %s!\n"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB2:
        .section        .text.startup,"ax",@progbits
.LHOTB2:
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB23:
        .cfi_startproc
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %edx
        movl    $.LC1, %esi
        movl    $1, %edi
        xorl    %eax, %eax
        call    __printf_chk
        xorl    %eax, %eax
        addq    $8, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE23:
        .size   main, .-main
        .section        .text.unlikely
.LCOLDE2:
        .section        .text.startup
.LHOTE2:
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

彙編語言的組成元素:指令、僞指令、標籤和註釋,每種元素獨佔一行

指令:

助記符 操作數(源,目的)

僞指令以"."開頭,末尾沒有冒號":"。僞指令是是輔助性的,彙編器在生成目標文件時會用到這些信息,但僞指令不是真正的 CPU 指令,就是寫給彙編器的。每種彙編器的僞指令也不同,要查閱相應的手冊。常見的彙編器僞指令如下。

.file   "hello.c"
	.section        .rodata.str1.1,"aMS",@progbits,1

標籤以冒號“:”結尾,用於對僞指令生成的數據或指令做標記。標籤很有用,它可以代表一段代碼或者常量的地址(也就是在代碼區或靜態數據區中的位置)。可一開始,我們沒法知道這個地址的具體值,必鬚生成目標文件後,才能算出來。所以,標籤會簡化彙編代碼的編寫。

.LC1:
        .string "Hello %s!\n"

註釋以“#”號開頭,與C語言中//表示註釋是一樣的。

二、指令

在代碼中,助記符movq,xorl中的movxor是指令,而ql叫做後綴,表示操作數的位數。後綴一共有 b, w, l, q 四種,分別代表 8 位、16 位、32 位和 64 位。

比如,movq 中的 q 代表操作數是 8 個字節,也就是 64 位的。movq 就是把 8 字節從一個地方拷貝到另一個地方,而 movl 則是拷貝 4 個字節。

而在指令中使用操作數,可以使用四種格式,它們分別是:立即數、寄存器、直接內存訪問和間接內存訪問。

操作數可以表示立即數(常數)值、寄存器值或是來自內存的值。比例因子\(s\)必須是1、2、4或者8.

立即數以 $ 開頭, 比如 $40。(下面這行代碼是把 40 這個數字拷貝到 %eax 寄存器)。

movl $40, %eax

除此之外,在指令中最常見到的就是對寄存器的訪問,GNU 的彙編器規定寄存器一定要以 % 開頭

直接內存訪問:當我們在代碼中看到操作數是一個數字時,它其實指的是內存地址。不要誤以爲它是一個數字,因爲數字立即數必須以 $ 開頭。另外,彙編代碼裏的標籤,也會被翻譯成直接內存訪問的地址。比如callq _printf中的_printf是一個函數入口的地址。彙編器幫我們計算出程序裝載在內存時,每個字面量和過程的地址。

間接內存訪問:帶有括號,比如(%rbp),它是指 %rbp 寄存器的值所指向的地址。

間接內存訪問的完整形式是:

偏移量(基址,索引值,字節數)這樣的格式。

其地址是:

基址 + 索引值 * 字節數 + 偏移量

舉例來說:

8(%rbp),是比 %rbp 寄存器的值加 8。

-8(%rbp),是比 %rbp 寄存器的值減 8。

(%rbp, %eax, 4)的值,等於 %rbp + %eax*4。這個地址格式相當於訪問 C 語言中的數組中的元素,數組元素是 32 位的整數,其索引值是 %eax,而數組的起始位置是 %rbp。其中字節數只能取 1,2,4,8 四個值。

幾個常用的指令:

數據傳輸指令

mov

mov 寄存器|內存|立即數, 寄存器|內存

這個指令最常用到,用於在寄存器或內存之間傳遞數據,或者把立即數加載到內存或寄存器。mov 指令的第一個參數是源,可以是寄存器、內存或立即數。第二個參數是目的地,可以是寄存器或內存。

lealea 是“load effective address”的意思,裝載有效地址,實際是mov指令的變形。其操作不影響任何條件碼

lea 源,目的

參數爲標準格式中給定的內存位置,但並不加載內存位置的內容,而是加載計算得出的地址。例如:如果寄存器%rdx的值爲x,那麼指令leaq 7(%rdx,%rdx,4),%eax將設置寄存器%rax的值爲5x+7

cld

該指令清除了標誌寄存器中的DF位。 清除方向標誌後,所有字符串操作(如stos,scas和其他操作)都會使索引寄存器esi或edi遞增。

std

與cld相反,該指令置位了標誌寄存器中的DF位。 置位方向標誌後,所有字符串操作(如stos,scas和其他操作)都會使索引寄存器esi或edi遞減。

stosl

stosl指令將eax複製到es:di中,若設置了EFLAGS中的方向位置位(即在STOSL指令前使用STD指令)則EDI自減4,否則(使用CLD指令)EDI自增4;

rep

重複執行%ecx次,如rep; stosl表示重複執行stosl,直到cx爲0,例:

cld;rep;stosl

cld設置edi或同esi爲遞增方向,rep做(%ecx)次重複操作,stosl表示edi每次增加4。

棧操作指令

指令 描述
push 源 把源壓入棧
pop 目的 把棧頂的元素放入目的

push

pushl %eax

相當於:

subl $4, %esp
mvol %eax,(%esp)
pushfl #表示將%eflage寄存器當前的數據入棧

pop

popl %eax

相當於:

movl (%esp), %eax
addl $4, %esp

運算指令

指令 描述
sub 源, 目的 把目的中值減去源的值
imul 源, 目的 把目的乘上源
clto 轉換爲8字(%rax符號擴展 →%rdx:%rax)
xor 源, 目的 做異或運算
or 源, 目的 或運算
and 源, 目的 與運算
inc 目的 加一
dec 目的 減一
neg 目的 取負值
not 目的 按位取反

add 指令是做加法運算,它可以採取下面的格式:

add 立即數, 寄存器 
add 寄存器, 寄存器 
add 內存, 寄存器 
add 立即數, 內存 
add 寄存器, 內存

比如,典型的 c=a+b 這樣一個算術運算可能是這樣的:

movl -4(%rbp), %eax    #把%rbp-4的值拷貝到%eax
addl -8(%rbp), %eax   #把%rbp-8地址的值加到%eax上
movl %eax, -12(%rbp)   #把%eax的值寫到內存地址%rbp-12

and 對兩個操作數的內容進行邏輯與運算,並將結果存儲到第二個操作數,將溢出標誌位及進位標誌設置爲FALSE。

not 對操作數的每一位邏輯取反,也稱爲一個數的補數

or 對兩個操作數進行邏輯或,並將結果存儲到第二個操作數,將溢出標誌位設置爲FLASE

adc 帶進位加法。將進位位與第一個操作數與第二個操作數相加,如果存在溢出,就將溢出及進位標誌設置爲真。

cdq將%eax中的字帶符號擴展爲%eax:%eax組成的雙字。q表示這是一個雙字(64字節).這條指令通常在發出idivl指令之前。

cmp 比較兩個整數,將第二個操作數減去第一個操作數,捨棄結果,設置標誌位。

dec將寄存器或內存位置的數據減一。

div執行無符號除法。將%edx:%eax所含的雙字除以指定寄存器或內存位置的值。運算後%eax包含商,%edx包含餘數,如果商對於%eax來說過大,導致溢出,將觸發中斷0.

idiv執行有符號除法。

imul執行有符號乘法,將結果保存到第二個操作數。如果第二個操作數空缺,就默認爲%eax,且完好的結果將存在%eax:%eax中

inc遞增給定寄存器或地址。

mul執行無符號乘法,運算規則與imull相同

neg將給定寄存器或內存位置的內容補齊(二進制求補)

sbb錯位減法,與adc用法相同。通常使用sub

sub將兩個操作數相減,用第二個操作數減去第一個操作數,將結果保存的到第二個操作數,本指令可用於有符號整數及無符號整數

位操作

rcl將第一個操作數,向左循環移位給定次數,第一個操作數可以是立即數或寄存器%cl。循環移位包含進位標誌,因此此指令實際上對33位而非32位進行操作。本指令將設置溢出標誌

rcr向右循環移位,其他與上一條指令相同

rol向左循環移位,本指令設置溢出標誌和進位標誌,但不會將進位位作爲循環移位的一部分。向左循環移位的次數可以通過立即尋址方式或寄存器%cl的值指定

ror向右循環移位,其他與上一條指令相同

sal算術左移,符號位移出至進位標誌,最低有效位填充0,其他位左移。與一般左移相同,移動位數通過立即尋址方式或是寄存器%cl指定。

sar算術右移(填上符號位),最低有效位移出至進位標誌,符號位被向右移入,並保留原符號位。其他位只是向右移。移動位數通過立即尋址方式或是寄存器%cl指定。

shl邏輯左移,將所有位左移(對符號位不做特殊處理).將最左一位推入進位標誌,移動位數通過立即尋址方式或是寄存器%cl指定。

shr邏輯右移,將所有位右移(對符號位不做特殊處理).將最右一位推入進位標誌,移動位數通過立即尋址方式或是寄存器%cl指定。

比較操作指令

指令 描述
cmp 源1, 源2 根據源1-源2設置狀態碼
test 源1, 源2 根據源1& 源2設置狀態碼

標誌寄存器

OF: 溢出標誌.最近的操作導致一個補碼溢出---正溢出或負溢出。

SF : 符號標誌.最近的操作得到的結果爲負數。

ZF:零標誌,最近的操作得出的結果爲0。

A 輔助進位標誌。

P 奇偶標誌,如果最後一個結果的低字節由偶數個1,此標誌爲真。

CF 進位標誌,最近的操作使最高位產生了進位。可用來檢查無符號操作的溢出。

指令 描述
cli、sti 清除IF標誌位(CLear Interrupt flag)、置位IF標誌(SeT Interrupt flag)
pushfq、popfq 將RFLAGS的值壓棧和出棧

例如,用一條ADD指令完成等價於t=a+b的功能,這裏a、b和t都是整型。然後根據結果來設置條件碼:

CF (unsigned) t < (unsigned) a 無符號溢出

ZF (t = 0)

SF (t < 0) 負數

OF (a < 0==b < 0) && (t < 0 !=a < 0) 有符號溢出

流控制指令

指令 描述
jmp 標籤或地址 跳轉到某個位置的代碼
call 標籤或地址 把返回地址壓入棧,並跳轉到指定位置的代碼
ret 從棧裏彈出返回地址,並跳轉過去

call 將%eip所指的下一個值入棧,並跳轉到目的地址。這用於函數調用。目的地址也可以是星號後跟寄存器的形式,這種方式爲間接函數調用。例如 call *%eax將調用%eax中所含地址所指的函數

int 引起給定數字的中斷。

jxx條件分支。xx爲條件(由前一條指令設置)爲TRUE,就跳轉到給定的地址;否則,執行下一條指令。條件代碼如下:

指令 含義 狀態碼
je或jz 跳轉,如果相等(等於零) ZF
jne或jnz 跳轉,如果不相等(等於零) ~ZF
js 跳轉,如果爲負值 SF
jns 跳轉,如果不爲負值 ~SF
jg或jnle 跳轉,如果大於,有符號數 ~(SF^OF) & ~ZF
jge或jnl 跳轉,如果大於等於,有符號數 ~(SF^OF)
jl或jnge 跳轉,如果小於,有符號數 SF^OF
jle或jne 跳轉,如果小於等於,有符號數 SF^OF | ZF
……
  • [n]a[e]—大於(無符號大於)、不大於、大於等於
  • [n]b[e]—小於(無符號小於)
  • [n]e—等於
  • [n]z—0
  • [n]g[e]—大於(帶符號比較)
  • [n]l[e]—小於(帶符號比較)
  • [n]c——進位標誌集
  • [n]o ——溢出標誌集
  • [p]p ——相等標誌集
  • [n]s ——符號標誌集
  • ecxz——%ecx爲0

jmp無條件跳轉。僅僅是將%eip設置爲目的地址,目的地址也可以是星號後跟寄存器的形式,這種方式爲間接函數調用。jmp *%eax用寄存器中的值作爲跳轉目標,jmp *(%rax)以內存地址%rax中的值作爲跳轉目標。

ret從棧種彈出值,並將%eip設置爲該值,用於從函數調用返回。

三、僞指令

.equ

.equ允許你爲數字分配名稱。例如

.equ LINUX_SYSCALL,0x80

此時LINUX_SYSCALL就是一個常量,使用如下

int $LINUX_SYSCALL

計算一段數據的長度

.section .data
helloworld:
	.ascii "hello world\n"
helloworld_end;
.equ helloworld_len, helloworld_end - helloworld

.rept

.rept用於填充每一項,.rept告訴彙編程序將.rept.endr之間的斷重複指定次數.

.rept 30  #填充30字節0
.byte 0
.endr

.endr

結束以.rept定義的重複節(section)

.lcomm

.locmm指令將創建一個符號,代指一個存儲位置。使用.lcomm創建一個符號my_buffer,代指.bss段中用作緩衝區的500字節存儲位置。

.section .bss
.locmm my_buffer, 500

movl $my_buffer, %ecx #將緩衝區地址加載到%ecx中

.globl

.globl聲明一個全局符號。

.globl _start
_start:

.type

.type指定一個符號作爲某種類型。例如告訴鏈接器 符號power作爲函數處理:

.type power, @function
power:
	……

如果其他程序中沒有使用該函數,則這條指令可以不需要。power:將下一條指令的存儲位置賦給符號power,這就是爲什麼調用該函數時需要如下執行:

call power

.ascii

將給定帶引號字符串轉換爲字節數據

.byte

將逗號分隔符的值列表作爲數據插入程序

.section

切換正在使用的節。通用節包括.text、.data、.bss

變量

定義一個long型變量begin如下:

begin:
	.long 0

四、X86_64寄存器

x86-64 架構的 CPU 裏有很多寄存器,我們在代碼裏最常用的是 16 個 64 位的通用寄存器,分別是:

%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp, %r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15

這些寄存器在歷史上有各自的用途,比如,rax 中的“a”,是 Accumulator(累加器) 的意思,這個寄存器是累加寄存器。

但隨着技術的發展,這些寄存器基本上都成爲了通用的寄存器,不限於某種特定的用途。但是,爲了方便軟件的編寫,我們還是做了一些約定,給這些寄存器劃分了用途。針對 x86-64 架構有多個調用約定(Calling Convention),包括微軟的 x64 調用約定(Windows 系統)、System V AMD64 ABI(Unix 和 Linux 系統)等,下面的內容屬於後者:

  • %rax 除了其他用途外,通常在函數返回的時候,把返回值放在這裏。

  • %rsp 作爲棧指針寄存器,指向棧頂。

  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 給函數傳整型參數,依次對應第 1 參數到第 6 參數。超過 6 個參數使用。

如果程序要使用 %rbx,%rbp,%r12,%r13,%r14,%r15 這幾個寄存器,是由被調用者(Callee)負責保護的,也就是寫到棧裏,在返回的時候要恢復這些寄存器中原來的內容。其他寄存器的內容,則是由調用者(Caller)負責保護,如果不想這些寄存器中的內容被破壞,那麼要自己保護起來。

上面這些寄存器的名字都是 64 位的名字,對於每個寄存器,我們還可以只使用它的一部分,並且另起一個名字。比如對於 %rax,如果使用它的前 32 位,就叫做 %eax,前 16 位叫 %ax,前 8 位(0 到 7 位)叫 %al,8 到 15 位叫 %ah。

原本含義 64位 32位 16位 高8位 低8位
Accumulator 累加器 rax eax ax ah al
Base 基地址 rbx ebx bx bh bl
Counter 計數器 rcx ecx cx ch cl
Data 數據 rdx edx dx dh dl
Source rsi esi si sil
Destination 目的 rdi edi di dil
Stack Base Pointer 棧基址 rbp ebp bp bpl
Stack Pointer 棧指針 rsp esp sp spi
後增加的8個通用寄存器 r8-r15 r8d-r15d r8w-r15w r8b-r15b

除了通用寄存器以外,有可能的話,還要了解下面的寄存器和它們的用途,我們寫彙編代碼時經常跟它們發生關聯:

  • 8 個 80 位的 x87 寄存器,用於做浮點計算;

  • 8 個 64 位的 MMX 寄存器,用於 MMX 指令(即多媒體指令),這 8 個跟 x87 寄存器在物理上是相同的寄存器。在傳遞浮點數參數的時候,要用 mmx 寄存器。

  • 16 個 128 位的 SSE 寄存器,用於 SSE 指令。 (SIMD )。

  • 指令寄存器,rip,保存指令地址。CPU 總是根據這個寄存器來讀取指令。

  • flags(64 位:rflags, 32 位:eflags)寄存器:每個位用來標識一個狀態。比如,它們會用於比較和跳轉的指令,比如 if 語句翻譯成的彙編代碼,就會用它們來保存 if 條件的計算結果。

五、常見彙編結構

1. 函數調用傳參

使用寄存器傳參

在 X86-64 架構下,有很多的寄存器,所以程序調用約定中規定儘量通過寄存器來傳遞參數,而且,只要參數不超過 6 個,都可以通過寄存器來傳參,使用的寄存器如下:

32位名稱 64位名稱 所傳參數
%eax %rax 參數1
%esi %esi 參數2
%edx %rdx 參數3
%ecx %rcx 參數4
%r8d %r8 參數5
%r9d %r9 參數6

使用棧傳參

超過 6 個的參數的話,要再加上棧來傳參:

根據程序調用約定的規定,參數 1~6 是放在寄存器裏的,參數 7 和 8 是放到棧裏的,函數參數以逆序的方向入棧,先放參數 8,再放參數 7。

int fun1(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8){
    int c = 10; 
    return x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + c;
}
println("fun1:" + fun1(1,2,3,4,5,6,7,8));
# function-call2-craft.s 函數調用和參數傳遞
    # 文本段,純代碼
    .section    __TEXT,__text,regular,pure_instructions
_fun1:
    # 函數調用的序曲,設置棧指針
    pushq   %rbp           # 把調用者的棧幀底部地址保存起來   
    movq    %rsp, %rbp     # 把調用者的棧幀頂部地址,設置爲本棧幀的底部
    movl    $10, -4(%rbp)  # 變量c賦值爲10,也可以寫成 movl $10, (%rsp)
    # 做加法
    movl    %edi, %eax     # 第一個參數放進%eax
    addl    %esi, %eax     # 加參數2
    addl    %edx, %eax     # 加參數3
    addl    %ecx, %eax     # 加參數4
    addl    %r8d, %eax     # 加參數5
    addl    %r9d, %eax     # 加參數6
    addl    16(%rbp), %eax  # 加參數7
    addl    24(%rbp), %eax  # 加參數8
    
    addl    -4(%rbp), %eax # 加上c的值
    # 函數調用的尾聲,恢復棧指針爲原來的值
    popq    %rbp           # 恢復調用者棧幀的底部數值
    retq                   # 返回
    .globl  _main          # .global僞指令讓_main函數外部可見
_main:                                  ## @main
    
    # 函數調用的序曲,設置棧指針
    pushq   %rbp           # 把調用者的棧幀底部地址保存起來  
    movq    %rsp, %rbp     # 把調用者的棧幀頂部地址,設置爲本棧幀的底部
    
    subq    $16, %rsp      # 這裏是爲了讓棧幀16字節對齊,實際使用可以更少
    # 設置參數
    movl    $1, %edi     # 參數1
    movl    $2, %esi     # 參數2
    movl    $3, %edx     # 參數3
    movl    $4, %ecx     # 參數4
    movl    $5, %r8d     # 參數5
    movl    $6, %r9d     # 參數6
    movl    $7, (%rsp)   # 參數7
    movl    $8, 8(%rsp)  # 參數8
    callq   _fun1                # 調用函數
    # 爲pritf設置參數
    leaq    L_.str(%rip), %rdi   # 第一個參數是字符串的地址
    movl    %eax, %esi           # 第二個參數是前一個參數的返回值
    callq   _printf              # 調用函數
    # 設置返回值。這句也常用 xorl %esi, %esi 這樣的指令,都是置爲零
    movl    $0, %eax
    addq    $16, %rsp    # 縮小棧
    
    # 函數調用的尾聲,恢復棧指針爲原來的值
    popq    %rbp         # 恢復調用者棧幀的底部數值
    retq                 # 返回
    # 文本段,保存字符串字面量                                  
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "fun1 :%d \n"

其棧幀的變化過程,如下:

使用棧來傳遞參數時,需要將函數參數以逆序的方向入棧,併發出call指令。調用後在將參數出棧:

printf("The numer is %d",88);
.section .data
test_string:
.ascii "The numer is %d\0"
.section .text

pushl $88
pushl $test_string
call printf
popl %eax
popl %eax

2. 變量賦值

彙編語言中全局變量訪問方式與局部變量不同。全局變量通過直接尋址訪問,而局部變量使用基址尋址方式,例如

int my_global_var;
int foo()
{
    int my_local_var;
    
    my_local_var = 1;
    my_glocal_var = 2;
    
    return 0
}

用匯編表示以上爲:

.section .data
.lcomm my_globl_var, 4

.type foo, @function
foo:
	pushl %ebp #保存原棧基址
	movl %esp, %ebp #令棧指針指向新基址指針
	subl $4, %esp #爲變量my_local_var保留空間
	.equ my_local_var, -4 #用my_local_var尋找局部變量
	
	movl $1, my_local_var(%ebp)
	movl $2, my_global_var
	
	movl %ebp, %esp #清除函數變量並返回
	popl %ebp
	ret

3. 指針

指針,它只是保存某個值的地址。全局變量:

int global_data = 30;

其對應的彙編爲:

.section .data
global_data:
	.long 30

C語言中取地址如下:

p = &global_data;

對應的彙編爲:

movl $global_data, %eax

可以看到彙編語言中總是通說指針訪問內存,也就是直接尋址方式,爲了取得指針本身,必須採用立即尋址方式。

局部變量略爲複雜,C語言代碼如下:

void foo()
{
    int a;
    int  *b;
    
    a = 30;
    
    b = &a;
    *b = 44;
}

對應彙編如下:

foo:
#標準函數開頭
	pushl %ebp
	movl %ebp,%esp
	
	#保留兩個字的內存
	subl -8, %ebp
	.equ A_VAR, -4
	.equ B_VAR, -8
	
	#a = 30
	movl $30, A_VAL(%ebp)
	
	#b = &a
	movl $A_VAR, B_VAR(%ebp)
	addl %ebp, B_VAR(%ebp)
	
	#*b = 30
	movl B_VAR(%ebp), %eax  #B
	movl $30, (%eax)
	
#標準結束函數
	movl %ebp ,%esp
	popl %ebp
	ret

要獲取局部變量的地址,必須按基址尋址方式計算該地址。還有更簡單的方式就是lea指令,該指令加載有效地址,會讓計算機計算地址,然後在需要的時候加上地址:

#b = &a
leal A_VAR(%ebp), %eax
movl %eax, B_VAR(%ebp)

4. 結構

結構時對內存塊的簡單描述,例如,在C語言中可以使用如下代碼:

struct person{
    char pristname[40];
    char lastname[40];
    int age
};

彙編中只是給予你一種使用84字節數據的方式。

.equ PERSON_SIZE, 84
.equ PERSON_FIRSTNAME_OFFSET, 0
.equ PERSON_LASTNAME_OFFSET, 40
.equ PERSON_AGE_OFFSET, 80

當聲明此類型的一個變量時,保留84字節空間就行,C代碼如下:

void foo()
{
    struct person p;
    /**/
    ……
}

對應的彙編代碼如下:

foo:
	#標準開頭
	pushl %ebp
	movl %esp, %ebp
	
	#爲局部變量分配空間
	subl $PERSON_SIZE, %esp 
	#這是變量相對於%ebp的偏移量
	.equ P_VAR, 0-PERSON_SIZE
	
	……
	#標準結束
	movl %ebp, %esp
	pop %ebp
	ret

訪問結構體成員,必須使用基址尋址方式,偏移量爲上面定義的值。如C語言設置年齡如下:

p.age = 30;

對應的彙編入下:

movl $30, P_VAR + PERSON_AGE_OFFSET(%ebp)

5. 循環

C語言語句如下:

while (a < b){
    /*某些操作*/
}
/*結束循環*/

這些對應的彙編如下所示:

loop_begin:
	movl a, %eax
	movl b, %ebx
	cmpl %eax, %ebx
	jge loop_end
	
	loop_body:
	#某些操作
	jmp loop_begin
	
loop_end:
#結束循環

上面說到寄存器%ecx可用作計數器,終止條件爲0,loop 指令會遞減%ecx,並在%ecx不爲0 的條件下跳轉到指定地址,例如,需執行某個語句100次C 語言如下:

for (i = 0; i < 100; i++){
   /*某些操作*/
}

彙編實現如下:

loop_initalize:
	movl 100,%ecx
loop_begin:
	#某些操作
	#遞減%ecx,若%ecx不爲0則繼續循環
	loop loop_begin
	
rest_of_program:

6. if語句

if(a == b){
    /*真分支操作*/
}else{
    /*假分支操作*/
}
/*真假匯合*/

在彙編中表示如下:

	#將a.b移入寄存器用於比較
    movl a, %eax
    movl b, %ebx

    #比較
    cmpl %eax, %ebx
    #跳轉到真分支
    je true_branch
fale_branch: #非必要標籤,只是爲了說明這是假分支
#假分支代碼

#跳到真假匯合
	jmp reconverge
true_branch:
	#真分支代碼
reconverge:

7. 浮點數使用

之前我們用的例子都是採用整數,現在使用浮點數來做運算。下面這段代碼:

float fun1(float a, float b){
    float c = 2.0;
    return a + b + c;
}

使用 -O2 參數,把 C 語言的程序編譯成彙編代碼如下:

.file   "float.c"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB1:
        .text
.LHOTB1:
        .p2align 4,,15
        .globl  fun1
        .type   fun1, @function
fun1:
.LFB0:
        .cfi_startproc
        addss   %xmm0, %xmm1	#浮點數傳參用XMM寄存器,加法用addss指令
        addss   .LC0(%rip), %xmm1 #把常量2.0加到xmm0上,xmm0保存返回值
        movaps  %xmm1, %xmm0
        ret
        .cfi_endproc
.LFE0:
        .size   fun1, .-fun1
        .section        .text.unlikely
.LCOLDE1:
        .text
.LHOTE1:
        .section        .rodata.cst4,"aM",@progbits,4
        .align 4
.LC0:
        .long   1073741824  ## float 2 常量
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

這個代碼的結構你應該熟悉了,棧幀的管理方式都是一樣的,都要維護 %rbp 和 %rsp。不一樣的地方,有幾個地方:

  • 傳參。給函數傳遞浮點型參數,是要使用 XMM 寄存器。

  • 指令。浮點數的加法運算,使用的是 addss 指令,它用於對單精度的標量浮點數做加法計算,這是一個 SSE1 指令。SSE1 是一組指令,主要是對單精度浮點數 (比如 C 或 Java 語言中的 float) 進行運算的,而 SSE2 則包含了一些雙精度浮點數(比如 C 或 Java 語言中的 double)的運算指令。

  • 返回值。整型返回值是放在 %eax 寄存器中,而浮點數返回值是放在 xmm0 寄存器中的。調用者可以從這裏取出來使用。

六、C嵌入彙編

爲什麼要使用匯編語言有兩個可能的原因。首先是,當我們接近硬件時,C受到限制。例如。沒有C語句可直接修改處理器狀態寄存器。第二個原因是創建高度優化的代碼。(以下內容大多來源於GCC-Inline-Assembly-HOWTO.)

1.基本內嵌

基本內聯彙編的格式非常簡單。它的基本形式是

asm("assembly code");

例如:

asm("movl %ecx %eax"); /* moves the contents of ecx to eax */
__asm__("movb %bh (%eax)"); /*moves the byte from bh to the memory pointed by eax */

在這裏使用過asm__asm__。兩者都有效。如果關鍵字asm與我們程序中的某些內容衝突,我們可以使用__asm__。如果我們有多個指令,我們用雙引號每行寫一個,並在指令後面加上'\ n'和'\ t'。這是因爲gcc將每個指令作爲一個字符串發送至彙編器。如下:

__asm__ ("movl %eax, %ebx\n\t"
          "movl $56, %esi\n\t"
          "movl %ecx, $label(%edx,%ebx,$4)\n\t"
          "movb %ah, (%ebx)");

如果在我們的代碼中,我們更改一些寄存器中的內容並從asm返回而不恢復爲修改前的值,則會發生一些不好的事情。這是因爲GCC不知道寄存器內容的變化,這導致我們遇到麻煩,特別是當編譯器進行一些優化時。它會假設某些寄存器包含某些變量的值,我們可能在沒有通知GCC的情況下對其進行了更改,並且它仍然沒有發生任何事情。我們可以做的是使用那些沒有副作用的指令或在我們退出或等待某些事情崩潰時解決問題。這是我們想要一些擴展功能的地方。擴展的asm爲我們提供了該功能。

2.擴展內嵌彙編

在基本的內聯彙編中,只有指令。在擴展彙編中,我們還可以指定操作數。它允許我們指定輸入寄存器,輸出寄存器和破壞寄存器列表。指定要使用的寄存器並不是強制性的,我們可以將這個問題留給GCC,這可能更適合GCC的優化方案。無論如何,基本格式是:

asm ( assembler template
    : output operands               /*optional*/
    : input operands                /*ooptional*/
    : list of clobbered registers       /*ooptional*/
    );
  • assembler template由彙編指令組成.
  • 每個操作數由操作數約束字符串描述,後跟括號中的C表達式。
  • 冒號將彙編程序模板與第一個輸出操作數分隔開,另一個冒號將最後一個輸出操作數與第一個輸入分開,如果有的話。
  • 逗號分隔每個組中的操作數。操作數的總數限制爲10或機器描述中任何指令模式中的最大操作數數量,以較大者爲準。

asm_(

彙編語句模版:

輸出部分:

輸入部分:

破壞描述部分:);

例如:

 int a=10, b;
 asm ("movl %1, %%eax; 
     movl %%eax, %0;"
     :"=r"(b)        /* output */
     :"r"(a)         /* input */
     :"%eax"         /* clobbered register */
     );      

這裏我們做的是使用匯編指令使'b'的值等於'a'的值。一些興趣點是:

  • “b”是輸出操作數,由%0引用,“a”是輸入操作數,由%1引用。
  • “r”是對操作數的約束。我們稍後會詳細介紹約束。目前,“r”向GCC表示使用任何寄存器來存儲操作數。輸出操作數約束應該有一個約束脩飾符“=”。而這個修飾符表示它是輸出操作數並且是隻寫的。
  • 寄存器名稱前面有兩個%的前綴。這有助於GCC區分操作數和寄存器。操作數具有單個%作爲前綴。
  • 第三個冒號後的修改後的寄存器%eax告訴GCC%eax的值將在“asm”內修改,因此GCC不會使用該寄存器來存儲任何其他值。

當“asm”的執行完成時,“b”將反映更新的值,因爲它被指定爲輸出操作數。換句話說,“asm”中對“b”的改變應該反映在“asm”之外。

現在我們可以詳細查看每個字段。

2.1彙編模板

彙編程序模板包含插入C程序內的彙編指令集。格式如下:要麼每條指令都用雙引號括起來,要麼整個指令組都在雙引號內。每條指令也應以分隔符結束。有效分隔符是換行符(\ n)和分號(;)。'\ n'後面可能跟一個標籤(\ t)。對應於C表達式的操作數由%0,%1 ......等表示。

2.2操作數

C表達式用作“asm”中的彙編指令的操作數。每個操作數都被寫爲雙引號中的第一個操作數約束。對於輸出操作數,在引號內也會有一個約束脩飾符,然後是C表達式,它代表操作數。即

“約束”(C表達式)是一般形式。對於輸出操作數,將有一個額外的修飾符。約束主要用於決定操作數的尋址模式。它們還用於指定要使用的寄存器。

如果我們使用多個操作數,則用逗號分隔。

在彙編程序模板中,每個操作數都由數字引用。編號如下進行。如果總共有n個操作數(包括輸入和輸出),則第一個輸出操作數編號爲0,按遞增順序繼續,最後一個輸入操作數編號爲n-1。最大操作數是我們在上一節中看到的。

輸出操作數表達式必須是左值。輸入操作數不受此限制。他們可能是表達式。擴展的asm功能最常用於編譯器本身不知道存在的機器指令;-)。如果無法直接尋址輸出表達式(例如,它是位字段),則我們的約束必須允許寄存器。在這種情況下,GCC將使用寄存器作爲asm的輸出,然後將該寄存器內容存儲到輸出中。

如上所述,普通輸出操作數必須是只寫的; GCC將假設在指令之前這些操作數中的值已經死亡且無需生成。擴展的asm還支持輸入輸出或讀寫操作數。

所以現在我們專注於一些例子。我們想要將數字乘以5。爲此,我們使用指令lea

asm ("leal (%1,%1,4), %0"
          : "=r" (five_times_x)
          : "r" (x) 
          );

這裏我們的輸入是'x'。我們沒有指定要使用的寄存器。GCC將選擇一些輸入寄存器,一個用於輸出,並按我們的意願行事。如果我們希望輸入和輸出駐留在同一個寄存器中,我們可以指示GCC這樣做。這裏我們使用那些類型的讀寫操作數。通過指定適當的約束,例如。

sm ("leal (%0,%0,4), %0"
          : "=r" (five_times_x)
          : "0" (x) 
          );

現在輸入和輸出操作數在同一個寄存器中。但是我們不知道哪個寄存器。現在,如果我們也要指定它,那麼有一種方法。

asm ("leal (%%ecx,%%ecx,4), %%ecx"
          : "=c" (x)
          : "c" (x) 
          );

從GCC 3.1版開始,GCC編譯器就支持符號名稱,在代碼部分,操作數由百分號引用,後跟方括號內的相關符號名。它引用包含相同符號名的操作數列表之一中的條目。如xenomai線程切換代碼示例:

static inline void do_switch_threads(struct xnarchtcb *out_tcb,
				     struct xnarchtcb *in_tcb,
				     struct task_struct *outproc,
				     struct task_struct *inproc)
{
	long ebx_out, ecx_out, edi_out, esi_out;

	__asm__ __volatile__("pushfl\n\t"
			     "pushl %%ebp\n\t"
			     "movl %[spp_out_ptr],%%ecx\n\t"
			     "movl %%esp,(%%ecx)\n\t"
			     "movl %[ipp_out_ptr],%%ecx\n\t"
			     "movl $1f,(%%ecx)\n\t"
			     "movl %[spp_in_ptr],%%ecx\n\t"
			     "movl %[ipp_in_ptr],%%edi\n\t"
			     "movl (%%ecx),%%esp\n\t"
			     "pushl (%%edi)\n\t"
			     __CANARY_SWITCH
			     "jmp  __switch_to\n\t"
			     "1: popl %%ebp\n\t"
			     "popfl\n\t"
			     : "=b"(ebx_out),
			       "=&c"(ecx_out),
			       "=S"(esi_out),
			       "=D"(edi_out),
			       "+a"(outproc),
			       "+d"(inproc)
			       __CANARY_OUTPUT
			     : [spp_out_ptr] "m"(out_tcb->spp),
			       [ipp_out_ptr] "m"(out_tcb->ipp),
			       [spp_in_ptr] "m"(in_tcb->spp),
			       [ipp_in_ptr] "m"(in_tcb->ipp)
			       __CANARY_INPUT
			     : "memory");
}

其中的asm符號操作符使用單獨的名稱空間。也就是說,與C代碼中的符號無關。但是必須在每個asm語句的符號必須唯一。

2.3 Clobber列表

一些指令破壞了一些硬件寄存器。我們必須在clobber-list中列出這些寄存器,即asm函數中第三個' ' 之後的字段。這是爲了告知gcc我們將自己使用和修改它們。所以gcc不會假設它加載到這些寄存器中的值是有效的。我們不應該在此列表中列出輸入和輸出寄存器。因爲,gcc知道“asm”使用它們(因爲它們被明確指定爲約束)。如果指令隱式或顯式地使用任何其他寄存器(並且輸入或輸出約束列表中不存在寄存器),則必須在破壞列表中指定這些寄存器。

如果我們的指令可以改變條件代碼寄存器,我們必須將“cc”添加到破壞寄存器列表中。

如果我們的指令以不可預測的方式修改內存,請將“memory”添加到修飾寄存器列表中。這將導致GCC不在彙編器指令的寄存器中保持緩存的內存值。如果受影響的內存未在asm的輸入或輸出中列出,我們還必須添加volatile關鍵字。

我們可以根據需要多次讀取和編寫被破壞的寄存器。考慮模板中多個指令的示例; 它假定子程序_foo接收在寄存器eaxecx參數的參數。

asm ("movl %0,%%eax;
              movl %1,%%ecx;
              call _foo"
             : /* no outputs */
             : "g" (from), "g" (to)
             : "eax", "ecx"
             );

2.4 Volatile

如果您熟悉內核源代碼或類似的一些漂亮的代碼,您必須已經看到許多函數聲明爲volatile__volatile__。我之前提到過關鍵字asm__asm__。那這 volatile是什麼?

如果我們的彙編語句必須在我們放置的地方執行,(即不能作爲優化移出循環),請將關鍵字volatile放在asm之後和()之前。我們將其聲明爲

asm volatile ( ... : ... : ... : ...);

使用__volatile__的時候,我們必須非常小心。

C編譯器的代碼優化器也會優化內嵌asm代碼。如果我們的程序集只是用於進行一些計算並且沒有任何副作用,那麼最好不要使用關鍵字volatile。避免gcc無法優化代碼。

在 一些有用的方法 中,我提供了許多內聯asm函數的示例。在那裏我們可以看到詳細的clobber列表。

2.5 常用約束

存在許多約束,其中僅頻繁使用少數約束。我們將看看這些約束。

  1. 註冊操作數約束(r)

使用此約束指定操作數時,它們將存儲在通用寄存器(GPR)中。採用以下示例:

asm ("movl %%eax, %0\n" :"=r"(myval));

這裏變量myval保存在寄存器中,寄存器中的值 eax被複制到該寄存器中,並且值myval從該寄存器更新到存儲器中。當指定“r”約束時,gcc可以將變量保存在任何可用的GPR中。要指定寄存器,必須使用特定的寄存器約束直接指定寄存器名稱。他們是:

+---+--------------------+
| r |    Register(s)     |
+---+--------------------+
| a |   %eax, %ax, %al   |
| b |   %ebx, %bx, %bl   |
| c |   %ecx, %cx, %cl   |
| d |   %edx, %dx, %dl   |
| S |   %esi, %si        |
| D |   %edi, %di        |
+---+--------------------+
  1. 內存操作數約束(m)

當操作數在存儲器中時,對它們執行的任何操作將直接發生在存儲器位置,而不是寄存器約束,寄存器約束首先將值存儲在要修改的寄存器中,然後將其寫回存儲器位置。但是寄存器約束通常僅在它們對於指令絕對必要時才使用,或者它們顯著加速了該過程。在需要在“asm”內更新C變量並且您真的不想使用寄存器來保存其值時,可以最有效地使用內存約束。例如,idtr的值存儲在內存位置loc中:

asm("sidt %0\n" : :"m"(loc));
  1. 匹配(數字)約束

在某些情況下,單個變量可以作爲輸入和輸出操作數。可以通過使用匹配約束在“asm”中指定這種情況。

asm ("incl %0" :"=a"(var):"0"(var));

我們在操作數小節中也看到了類似的例子。在此示例中,匹配約束,寄存器%eax用作輸入和輸出變量。var輸入讀取到%eax,更新後%eax在增量後再次存儲在var中。這裏的“0”指定與第0個輸出變量相同的約束。也就是說,它指定var的輸出實例應僅存儲在%eax中。可以使用此約束:

  • 在從變量讀取輸入或修改變量並將修改寫回同一變量的情況下。
  • 如果不需要輸入和輸出操作數的單獨實例。

使用匹配約束的最重要的影響是它們導致有效使用可用寄存器。

  1. memory

volatile

將volatile屬性添加到asm語句中,以指示編譯器不要優化彙編部分代碼。memory**

它告訴編譯器彙編程序指令可能會更改內存位置。將強制編譯器在執行彙編程序指令之前先保存緩存裏的值到內存,然後在加載它們。並且保留執行順序,因爲在使用內存破壞者執行asm語句後,所有變量的內容都是不可預測的。

使所有緩存的值無效可能不是最佳的。除此之外,可以添加一個虛擬操作數來創建人工依賴項:

2.6約束脩飾符

在使用約束時,爲了更精確地控制約束的影響,GCC爲我們提供了約束脩飾符。最常用的約束脩飾符是

  1. “=”:表示該操作數對該指令是隻寫的; 先前的值被丟棄並由輸出數據替換。

  2. “&”:表示此操作數是一個earlyclobber操作數,在使用輸入操作數完成指令之前修改該操作數。因此,該操作數可能不在於用作輸入操作數的寄存器或任何存儲器地址的一部分。如果輸入操作數僅用作輸入,則在寫入早期結果之前,它可以綁定到earlyclobber操作數。

更多約束描述

2.7 一些有用的方法

現在我們已經介紹了關於GCC內聯彙編的基本理論,現在我們將集中討論一些簡單的例子。將內聯asm函數編寫爲MACRO總是很方便。我們可以在內核代碼中看到許多asm函數。(/usr/src/linux/include/asm/*.h)。

  1. 首先,我們從一個簡單的例子開始。我們將編寫一個程序來相加兩個數字。
int main(void)
{
        int foo = 10, bar = 15;
        __asm__ __volatile__("addl  %%ebx,%%eax"
                             :"=a"(foo)
                             :"a"(foo), "b"(bar)
                             );
        printf("foo+bar=%d\n", foo);
        return 0;
}

在這裏,我們堅持要求GCC在%eax中存儲foo,bar存儲在%ebx中,我們也希望結果保存在%eax中。'='符號表示它是輸出寄存器。現在我們可以用其他方式實現變量和整數相加。

__asm__ __volatile__(
                      "   lock       ;\n"
                      "   addl %1,%0 ;\n"
                      : "=m"  (my_var)
                      : "ir"  (my_int), "m" (my_var)
                      :                                 /* no clobber-list */
                      );

這是一個原子加法。我們可以刪除指令'lock'來刪除原子性。在輸出字段中,“= m”表示my_var是輸出,它在內存中。類似地,“ir”表示,my_int是一個整數,應該駐留在某個寄存器中(回想一下我們上面看到的表)。clobber列表中沒有寄存器。

  1. 現在我們將對一些寄存器/變量執行一些操作並比較該值。
__asm__ __volatile__(  "decl %0; sete %1"
                      : "=m" (my_var), "=q" (cond)
                      : "m" (my_var) 
                      : "memory"
                      );

這裏,my_var的值減1,如果結果值是0 ,則設置變量cond。我們可以通過添加指令“lock; \ n \ t”作爲彙編程序模板中的第一條指令來添加原子性。

以類似的方式,我們可以使用“incl%0”而不是“decl%0”,以便增加my_var。

這裏要注意的是(i)my_var是駐留在內存中的變量。(ii)約束“= q”保證了cond在eax,ebx,ecx和edx寄存器其中之一中。(iii)我們可以看到內存在clobber列表中。即,代碼正在改變內存的內容。

  1. 如何設置/清除寄存器中的位?
__asm__ __volatile__(   "btsl %1,%0"
                      : "=m" (ADDR)
                      : "Ir" (pos)
                      : "cc"
                      );

這裏,ADDR變量位置'pos'處的位(存儲器變量)設置爲1, 我們可以使用'btrl'代替'btsl'來清除該位。pos的約束“Ir”表示pos位於寄存器中,其值的範圍爲0-31(x86依賴約束)。也就是說,我們可以在ADDR設置/清除變量的第0到第31位。由於條件代碼將被更改,我們將“cc”添加到clobberlist。

  1. 現在我們來看一些更復雜但有用的功能。字符串副本。
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__(  "1:\tlodsb\n\t"
                       "stosb\n\t"
                       "testb %%al,%%al\n\t"
                       "jne 1b"
                     : "=&S" (d0), "=&D" (d1), "=&a" (d2)
                     : "0" (src),"1" (dest) 
                     : "memory");
return dest;
}

源地址存儲在esi中,目標位於edi中,然後啓動複製,當我們達到0時,複製完成。約束“&S”,“&D”,“&a”表示寄存器esi,edi和eax是early clobber寄存器,即它們的內容將在函數完成之前改變。這裏也很清楚爲什麼memory在clobberlist中。

我們可以看到一個類似的函數移動一個double words。請注意,該函數聲明爲宏。

#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ (                                          \
                       "cld\n\t"                                \
                       "rep\n\t"                                \
                       "movsl"                                  \
                       :                                        \
                       : "S" (src), "D" (dest), "c" (numwords)  \
                       : "%ecx", "%esi", "%edi"                 \
                       )

這裏我們沒有輸出,因此寄存器ecx,esi和edi的內容發生的變化是塊移動的副作用。所以我們必須將它們添加到clobber列表中.

  1. 在Linux中,使用GCC內聯彙編實現系統調用。讓我們看看如何實現系統調用。所有系統調用都寫成宏(linux / unistd.h)。例如,具有三個參數的系統調用被定義爲宏,如下所示。
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile (  "int $0x80" \
                  : "=a" (__res) \
                  : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
                    "d" ((long)(arg3))); \
__syscall_return(type,__res); \
}

每當進行具有三個參數的系統調用時,上面顯示的宏用於進行調用。系統調用號放在eax中,然後是ebx,ecx,edx中的每個參數。最後,“int 0x80”是使系統調用工作的指令。可以從eax收集返回值。

每個系統調用都以類似的方式實現。退出是一個單個參數系統調用,讓我們看看它的代碼是什麼樣的。它如下所示。

{
        asm("movl $1,%%eax;         /* SYS_exit is 1 */
             xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
             int  $0x80"            /* Enter kernel mode */
             );
}

退出的數量是“1”,這裏,它的參數是0。所以我們安排eax包含1和ebx包含0和by int $0x80exit(0)執行。這就是退出的方式。

七、編譯

彙編與鏈接

彙編:

as -o xxxx xxxx.s
as -32 -o xxxx xxxx.s #彙編32位

將彙編代碼彙編成動態鏈接庫(-shared):

ld -shared xxx.o yyy.o -o libzzz.so

使用動態鏈接。

ld -dynamic-linker /lib/ld-linux.so.2 -o xxxx xxxx.o -lc -lzzz

-lc表示鏈接到庫c,也就是libc.so-dynamic-linker鏈接動態庫。

版權聲明:本文爲本文爲博主原創文章,轉載請註明出處。如有問題,歡迎指正。博客地址:https://www.cnblogs.com/wsg1100/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章