GCC 提供了內嵌彙編的功能,可以在 C 代碼中直接內嵌彙編語言語句,大大方便了程序設計。簡單的內嵌彙編很容易理解,例:
__asm__ __volatile__("hlt");
"__asm__" 表示後面的代碼爲內嵌彙編,“asm”是“__asm__”的別名。
“__volatile__” 表示編譯器不要優化代碼,後面的指令保留原樣,“volatile”是它的別名。 括號裏面是彙編指令。
我們的目的是要理解這兩條語句
1. asm volatile ("inb %1, %0" : "=a" (data) : "d" (port) : "memory");
2. asm volatile (
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
內嵌彙編有一個語法模板
__asm__(
彙編語句模板:
輸出部分:
輸入部分:
破壞描述部分)
下面是一個具體的實際例子
__asm__ __volatile__(
"cli": ------>這個是彙編指令部分
: ------->這個是輸出部分,爲空
: -------->這個是輸入部分,爲空
"memory" -------->這個是破壞描述部分
)
下面按照順序來依次進行講解每一個部分大概的功能。
備註: 彙編語句模板必不可少,其他三部分可選,如果使用了後面的部分,而前面部分爲空,也需要用“:”格開,相應部分內容爲空。例如:
__asm__ __volatile__(
"cli":
:
:"memory")
彙編語句模板
彙編語句模板由彙編語句序列組成,語句之間使用“;”、“\n”或“\n\t”分開
我們可以使用一個例子看一下這幾個分隔符的區別?
int main()
{
__asm__ __volatile__ (
"cld\n\t"
"cld\n\t"
);
return 0;
}
查看一下預處理階段又沒有處理
GCC編程四個過程:預處理-編譯-彙編-鏈接
http://hi.baidu.com/hp_roc/blog/item/91691146c40de946500ffe39.html
下面是預處理的結果
sgy@ubuntu:~/sgy/user_program/test$ gcc -E test.c
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "test.c"
int main()
{
__asm__ __volatile__ (
"cld\n\t"
"cld\n\t"
);
return 0;
}
sgy@ubuntu:~/sgy/user_program/test$
我們發現預處理階段其實並沒有處理,我們看一下編譯階段幹了什麼
sgy@ubuntu:~/sgy/user_program/test$ cat test.s
.file "test.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
#APP
# 4 "test.c" 1
cld
cld
# 0 "" 2
#NO_APP
movl $0, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
sgy@ubuntu:~/sgy/user_program/test$
我們發現嵌入彙編真正處理的階段是在編譯階段做的, 我們把test.c 更改一下,把"\n\t" 替換成 ";"號
int main()
{
__asm__ __volatile__ (
"cld;"
"cld;"
);
return 0;
}
看一下最終的編譯結果
#APP
# 4 "test.c" 1
cld;cld;
# 0 "" 2
#NO_APP
用gcc直接編譯成可執行文件也不會出錯
sgy@ubuntu:~/sgy/user_program/test$ gcc test.c
sgy@ubuntu:~/sgy/user_program/test$
我們再把test.c 裏面的";" 替換成 “\n”
#APP
# 4 "test.c" 1
cld
cld
# 0 "" 2
#NO_APP
這三種結果只是在排版上面會出現不一樣,其實並不影響結果的編譯和運行。
但是明顯"\n\t" 效果最好,因爲排版最好看。
asm volatile ("inb %1, %0" : "=a" (data) : "d" (port) : "memory");
ok,這條語句的第一部分是彙編指令部分,且只有一條彙編指令,我們已經掌握了。那麼 %1, %0是什麼意思?
0,1 數字代表依次從 輸出部分開始的變量的編號,這個是對應的, 例如%0表示的就是data變量, %1代表的是port變量
"=a" (data) 這個是什麼意思呢?
等於號:表示其是一個輸出操作數,例如賦值給data,或更改data這個變量的值
每個輸出操作數的限定字符串必須包含“=”表示它是一個輸出操作數
後面緊跟着的a是限定字符, 表示輸出操作數要放入到eax寄存器裏面
其他的各個字符所代表的意思
“b”將輸入變量放入 ebx
“c”將輸入變量放入 ecx
“d”將輸入變量放入 edx
即port變量需要和 edx寄存器綁定起來
最後面的那個memory是個什麼意思。
一些需要的背景知識
編譯器會對代碼進行優化,以提高代碼的運行效率和速度(他們一種優化的方式是將指令亂序執行, 另外一種方式是使用緩存),但是這些編譯優化畢竟不是萬能的,某些硬件設備要求一部分指令按照特定的順序執行,所以不能優化他們。
linux 提供了一個宏解決編譯器的執行順序問題。
void Barrier(void)
這個函數通知編譯器插入一個內存屏障,但對硬件無效,編譯後的代碼會把當前 CPU寄存器中的所有修改過的數值存入內存,需要這些數據的時候再重新從內存中讀出。所以就不會使用到緩存裏面的內容。
基於上面的這些介紹,memory的功能主要如下
(1)不要將該段內嵌彙編指令與前面的指令重新排序;也就是在執行內嵌彙編代碼之前,它前面的指令都執行完畢
(2)不要將變量緩存到寄存器,因爲這段代碼可能會用到內存變量,而這些內存變量會以不可預知的方式發生改變,因此 GCC 插入必要的代碼先將緩存到寄存器的變量值寫回內存,如果後面又訪問這些變量,需要重新訪問內存。
我們可以把上面那一條指令通過反彙編來進行查看,看一下翻譯成彙編究竟長什麼樣子,稍微修改了一下原先代碼的樣子
int add(int port) {
int data;
asm volatile ("add %1, %0" : "=a" (data) : "d" (port) : "memory");
return data;
}
int main()
{
add(80);
return 0;
}
我們可以看一下add函數的彙編代碼是什麼樣子的。
(gdb) si
4 asm volatile ("add %1, %0" : "=a" (data) : "d" (port) : "memory");
1: x/6i $pc
=> 0x80483f3 <add+6>: mov 0x8(%ebp),%eax
0x80483f6 <add+9>: mov %eax,%edx
0x80483f8 <add+11>: add %edx,%eax
0x80483fa <add+13>: mov %eax,-0x4(%ebp)
0x80483fd <add+16>: mov -0x4(%ebp),%eax
0x8048400 <add+19>: leave
(gdb)
我們查看一下port和data變量的地址
(gdb) p /x &port
$1 = 0xbffff0d4
(gdb) p /x &data
$2 = 0xbffff0c8
這兩條指令是用來取出port變量的
=> 0x80483f3 <add+6>: mov 0x8(%ebp),%eax
0x80483f6 <add+9>: mov %eax,%edx
ebp的值是多少
(gdb) info reg ebp
ebp 0xbffff0cc 0xbffff0cc
(gdb)
ebp + 8 = 0xbffff0d4 剛好是port變量的地址, 一開始是將port變量放到eax寄存器裏面,但是發現與port變量綁定的是edx寄存器,所以又把edx寄存器放到了eax寄存器裏面,
最後的那一條add指令,說明data變量確實是與eax寄存器綁定的。
0x80483f8 <add+11>: add %edx,%eax
我們可以把上面的a,d, 改成b,c嘗試一下。data變量和ebx綁定,port和ecx綁定
asm volatile ("add %1, %0" : "=b" (data) : "c" (port) : "memory");
翻譯成彙編指令的結果如下
movl 8(%ebp), %eax
movl %eax, %ecx
#APP
# 4 "test.c" 1
add %ecx, %ebx
# 0 "" 2
#NO_APP
第二條指令是什麼意思呢?
static inline void
insl(uint32_t port, void *addr, int cnt) {
asm volatile (
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}
cld指令是使DF=0, 即si,di寄存器自動增加
SI(Source Index):源變址寄存器可用來存放相對於DS段之源變址指針;
DI(Destination Index):目的變址寄存器,可用來存放相對於 ES 段之目的變址指針。
關於repe指令的相關介紹,
rep指令的目的是重複其上面的指令.ECX的值是重複的次數.repe和repne,前者是repeat equal,意思是相等的時候重複,後者是repeat not equal,不等的時候重複;每循環一次cx自動減一。
insl指令的意思
insl 從 DX 指定的 I/O 端口將雙字輸入 ES:(E)DI 指定的內存位置
這個函數編譯成彙編長什麼樣子
080483ed <insl>:
static void
insl(int port, void *addr, int cnt) {
80483ed: 55 push %ebp
80483ee: 89 e5 mov %esp,%ebp
80483f0: 57 push %edi
80483f1: 53 push %ebx
asm volatile (
80483f2: 8b 55 08 mov 0x8(%ebp),%edx
80483f5: 8b 4d 0c mov 0xc(%ebp),%ecx
80483f8: 8b 45 10 mov 0x10(%ebp),%eax
80483fb: 89 cb mov %ecx,%ebx
80483fd: 89 df mov %ebx,%edi
80483ff: 89 c1 mov %eax,%ecx
8048401: fc cld
8048402: f2 6d repnz insl (%dx),%es:(%edi)
8048404: 89 c8 mov %ecx,%eax
8048406: 89 fb mov %edi,%ebx
8048408: 89 5d 0c mov %ebx,0xc(%ebp)
804840b: 89 45 10 mov %eax,0x10(%ebp)
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}
804840e: 5b pop %ebx
804840f: 5f pop %edi
8048410: 5d pop %ebp
8048411: c3 ret
這個函數與上面唯一不同的地方就在於輸入部分多了幾個匹配限制字符, 0,1我們就稱之爲匹配限制符
"0" (addr), "1" (cnt)
而且你發現addr和cnt既出現在輸出部分,又出現在輸入部分,這是爲什麼?
我們下面舉一個例子進行講解
extern int input,result;
void test_at_t()
{
result= 0;
input = 1;
__asm__ __volatile__ ("addl %1,%0":"=r"(result): "r"(input));
}
“r”將輸入變量放入通用寄存器,也就是 eax , ebx, ecx,edx, esi, edi 中的一個
他所對應的彙編代碼如下
movl $0, result
movl $1, input
movl input, %eax
#APP
# 7 "test.c" 1
addl %eax,%eax
# 0 "" 2
#NO_APP
movl %eax, result
popl %ebp
上面這條彙編指令執行的結果是什麼,你會發現結果是2,這顯然不對啊,結果應該是1啊。出現這樣的結果是爲什麼呢?
因爲對於輸出部分的變量,對於gcc來講是輸出操作數,所以只會給他分配一個通用寄存器,而不會將原先result的值給取出來做計算,gcc認爲輸出操作數的原來的值沒有用,所以編譯上優化掉了。
那這樣的話我改成下面這樣呢?
extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__ __volatile__ ("addl %2, %0":"=r"(result):"r"(result),"m"(input));
}
查看一下他的彙編
movl $0, result
movl $1, input
movl result, %eax
#APP
# 7 "test.c" 1
addl input, %eax
# 0 "" 2
#NO_APP
movl %eax, result
這個看似正常,但其實存在問題,如果result分配的寄存器不是同一個就會出現問題,
movl _result,%edx-----輸入部分分配的edx
#APP
addl _input,%eax-------結果還是錯誤的
#NO_APP
movl %eax,%edx
其實感覺改成這樣應該也可以,這個地方只是猜測,沒有經過驗證。
extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__ __volatile__ ("addl %2, %0":"=b"(result):"b"(result),"m"(input));
}
彙編指令如下
movl $0, result
movl $1, input
movl result, %eax
movl %eax, %ebx
#APP
# 7 "test.c" 1
addl input, %ebx
# 0 "" 2
#NO_APP
所以爲了解決這個問題
gcc引入了一個匹配限制符,即告訴gcc,這個變量需要一直使用同一個寄存器,寫在輸入部分是爲了要他以前的值。0,1是爲了說明和佔位符數值相同的變量使用同一個寄存器,因爲他們是同一個變量
所以這裏的意思輸入部分的addr與輸出部分的addr是同一個變量,所以他們應該使用同一個寄存器。cnt變量也是同理。
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
所以上面那條嵌入彙編的意思,大概就能清楚了
port變量和edx綁定
cnt變量和ecx綁定
addr 變量和 edi綁定,每次遞增完,地址加4
即把端口號爲port 傳送到 addr,讀取cnt個字節
可能講解的不是很好,但是結合上面我使用的例子,和查看結果的命令,配合下面的文章,我覺得這篇文章寫得很好,以上也只是進行了一些概括和提煉。
AT&T彙編語言與GCC內嵌彙編簡介.pdf