2013年9月3日 星期二

Linux inline asm and Macro expansion

Macro expansion:
vc:
cl /P yuv420argb.c

linux:
C:\Progra~1\MinGW\bin\mingw32-g++ -E -P cpuid.c -o cpuid.i

prototype generate:
vc:
cl /Zg yuv420argb.c

linux:
C:\Progra~1\MinGW\bin\mingw32-g++ -c -P cpuid.c -aux-info cpuid.i

////////////////////////////////////////////////////////////////////////////////////
#GCC inline asm#
__asm__
(
"#code# \n\t"
: output operand list
: input operand list
: clobber list
);

也就是用 ':'分開的字串。
其中 'code' 的部份要存取 output operand list , input operand 的內容,是用 % 符號:
%0 代表存取 output operand%1 代表存去 input operand最後的'clobber'
是要告訴 compiler 有哪些 register 被這段 assembly code 修改了

__asm__
(
"\n\t"
:::"memory"
);
它向GCC聲明:“我對記憶體作了改動”,GCC在編譯的時候,
會將此因素考慮進去。我們看一看下面這個例子:

$ cat example1.c

int main(int argc, char* argv[])
{
int* p = (int*) argc;
(*p) = 9999;

if((*p) == 9999)
return 5;

return (*p);
}

在這段代碼中,那條內聯彙編是被注釋掉的。
在這條內聯彙編之前,記憶體指標p指向的記憶體被賦值為9999,
隨即在內聯彙編之後,一條if語句判斷p指向的記憶體與9999是否相等。
我們使用下面的命令行對其進行編譯:

$ gcc -O -S example1.c
選項-O表示優化編譯,我們還可以指定優化等級,
比如-O2表示優化等級為2;
選項-S表示將C/C++原始檔案編譯為彙編檔,
檔案名和C/C++文件一樣,只不過副檔名由.c變為.s。

$ cat example1.s

main:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax # int* p = (int*) argc
movl $9999, (%eax) # (*p) = 9999
movl $5, %eax # return 5
popl %ebp
ret

我們現在將clobber list中指定了memory,重新編譯,然後看一下相關的編譯結果。

$ cat example2.c

int main(int argc, char* argv[])
{
int* p = (int*) argc;
(*p) = 9999;

__asm__
(
"\n\t"
:::"memory"
);

if((*p) == 9999)
return 5;

return (*p);
}

$ gcc -O -S example2.c

$ cat example1.s

main:
pushl %ebp movl %esp, %ebp
> movl 8(%ebp), %eax  # int* p = (int*) argc
movl $9999, (%eax)  # (*p) = 9999
cmpl $9999, (%eax)  # if ((*p) == 9999)
jne .L2        # false
movl $5, %eax     # true, return 5
jmp .L3
.L2:
movl (%eax), %eax
.L3:
popl %ebp
ret

由於內聯彙編語句__asm__("\n\t":::"memory")向GCC聲明,
在此內聯彙編語句出現的位置記憶體內容可能了改變,
所以GCC在編譯時就不能像剛才那樣處理。
這次,GCC老老實實的將if語句生成了彙編代碼。

////////////////////////////////////////////////////////////////////////////////
Register

+---+--------------------+
| 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 |
+---+--------------------+


Some other constraints used are:

"m" : A memory operand is allowed, with any kind of address that the machine supports in general.
"o" : A memory operand is allowed, but only if the address is offsettable. ie, adding a small offset to the address
gives a valid address.
"V" : A memory operand that is not offsettable. In other words, anything that would fit the `m’
constraint but not the `o’constraint.
"i" : An immediate integer operand (one with constant value) is allowed. This includes symbolic constants
whose values will be known only at assembly time.
"n" : An immediate integer operand with a known numeric value is allowed. Many systems cannot support
assembly-time constants for operands less than a word wide. Constraints for these operands should
use ’n’ rather than ’i’.
"g" : Any register, memory or immediate integer operand is allowed, except for registers that are not general registers.


"r" : Register operand constraint, look table given above.
"q" : Registers a, b, c or d.
"I" : Constant in range 0 to 31 (for 32-bit shifts).
"J" : Constant in range 0 to 63 (for 64-bit shifts).
"K" : 0xff.
"L" : 0xffff.
"M" : 0, 1, 2, or 3 (shifts for lea instruction).
"N" : Constant in range 0 to 255 (for out instruction).
"f" : Floating point register
"t" : First (top of stack) floating point register
"u" : Second floating point register
"A" : Specifies the `a’ or `d’ registers. This is primarily useful for 64-bit integer values
intended to be returned with the `d’ register holding the most significant
bits and the `a’ register holding the least significant bits.

__asm__
(“incl %0”
:”=a”(var)
:“0”(var) //數字0
);
這個例子中eax寄存器被用來保存輸入也用來保存輸出變數。
輸入變數被讀入eax中,incl執行之後eax被跟新並且又保存到變數var中。
這兒的constraint ”0”指定使用和第零個("=a")輸出相同的寄存器。

//////////////////////////////////////////////////////////////////////////////////
Sample Demo

int a=10, b;

__asm__ volatile
(
"movl %1, %%eax \n\t"
"movl %%eax, %0 \n\t"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);

Here what we did is we made the value of ’b’ equal to that of ’a’ using assembly instructions.
Some points of interest are:

"b" is the output operand, referred to by %0.
"a" is the input operand, referred to by %1.

"=r" is a constraint on the operands. We’ll see constraints in detail later. For the time being,
"r" says to GCC to use any register for storing the operands. output operand constraint should
have a constraint modifier "=". And this modifier says that it is the output operand and is write-only.
There are two %’s prefixed to the register name.
This helps GCC to distinguish between the operands and registers.
operands have a single % as prefix.
The clobbered register %eax after the third colon tells GCC that the value of %eax is to be
modified inside "asm", so GCC won’t use this register to store any other value.

We can read and write the clobbered registers as many times as we like.
Consider the example of multiple instructions in a template; it assumes
the subroutine _foo accepts arguments in registers eax and ecx.

__asm__ volatile
(
"movl %0,%%eax \n\t"
"movl %1,%%ecx \n\t"
: //#no outputs#
: "g" (from), "g"(to)
: "eax", "ecx"
);

//////////////////////////////////////////////////////////////////////////

Constraint Modifiers
`='
意味著該指令的該運算元為只寫的:先前的值將被丟棄並且由輸出資料替換。


`+'
意味著該運算元可以由指令讀和寫。
當編譯器修訂運算元來滿足約束時,它需要知道哪些運算元為指令的輸入以及哪些為它的輸出。
‘=’表示一個輸出;‘+’表示一個運算元同時為輸入和輸出;所有其他運算元將被認為只是輸入。
如果你指定了‘=’或者‘+’,你要將它作為約束字串的第一個字元。


`&'
意味著(在一個特別的可選項中)該運算元為一個earlyclobber運算元,其在指令完成使用輸入運算元之前就被修改了。
因此該運算元可能不在被用作輸入運算元或者用作任何記憶體位址的一部分的寄存器中。
`&'只應用於其所在的可選項。在具有多個可選項的約束中,有時一個可選項需要'&',而其他的不需要。
例如,參見68000的'movdf' insn。
一個輸入運算元可以被限定為一個earlyclobber運算元,
如果它唯一的作為輸入的使用發生在早期結果被寫出之前。增加這種形式的可選項經常可以允許GCC來產生更好的代碼,
當只有一些輸出可以被earlyclobber影響時。例如,參見ARM的`mulsi3' insn。'&'不排除對'='的需要。


`%'
聲明指令對於該運算元和隨後的運算元是可交換的。這意味著編譯器可以交換兩運算元,如果有更廉價的方式來使得所有運算元都適合約束。這經常被用於實際上只有兩個運算元的加法指令中:結果必須放在一個參數中。這裏有個例子,是68000半字加指令如何被定義的:
(define_insn "addhi3"
[(set (match_operand:HI 0 "general_operand" "=m,r")
(plus:HI (match_operand:HI 1 "general_operand" "%0,0")
(match_operand:HI 2 "general_operand" "di,g")))]
...)
GCC只能處理在asm中的一個可交換對;如果你有更多的,編譯器將會失敗。注意如果兩個可選項嚴格相同,則不需要使用該修飾符;這只會在重載過程浪費時 間。該修飾符在寄存器分配之後,是不可操作的,所以在重載之後執行的define_peephole2和define_splits的結果不能依賴'%' 來進行insn匹配。


`#'
表示所有後續的字元,直到下一個逗號,作為約束都被忽略掉。它們只對選擇寄存器優先時有意義。


`*'
表示後續字元在選擇寄存器優先時應該被忽略掉。'*'對於重載沒有影響。
這裏有一個例子:68000有一條指令,用於在資料寄存器中符號擴展一個半字,並且還可以通過將其複製到一個位址寄存器中來符號擴展一個值。當每種寄存器 都可以被接受時,對於位址寄存器的約束相對不是很嚴格,所以最好是寄存器分配將位址寄存器作為其目標。因此,'*'被使用,以至於'd'約束字母(資料寄 存器)被忽略,當計算寄存器優先時。
(define_insn "extendhisi2"
[(set (match_operand:SI 0 "general_operand" "=*d,a")
(sign_extend:SI
(match_operand:HI 1 "general_operand" "0,g")))]


/////////////////////////////////////////////////////////////////////
earlyclobber運算元

在inline asm中,特別需要注意earlyclobber的情況。 earlyclobber的意思是說,某個輸出操作數,可能在gcc尚未用完所有的輸入運算元之前,就已經被寫入(inline asm的輸出操作)值了。 換言之,gcc尚未使用完全部的輸入運算元的時候,就已經對某些輸出操作數產生了輸出操作。

earlyclobber是整個gcc inline assembly語法中最晦澀難懂的地方,從而也是程式最容易有BUG的地方。讓我們看一個示例程式:

/* WARNING: WRONG! */
$ cat -n wrong.c
1 #include
2
3 int main(void)
4 {
5 int var1 = 5, var2 = 5;
6
7 asm volatile
8 (
9 "movl $10, %0\n\t"
10 "movl $20, %1"
11 : "=r" (var1)
12 : "r" (var2)
13 );
14
15 printf("var1 is %d\n", var1);
16 return 0;
17 }

在這個程式中,我們希望給var1變數結合一個寄存器,給var2結合另一個寄存器。 但是程式犯了兩個錯誤:

a) 與var2結合的寄存器是輸入運算元,但是第10行給它賦值了;
b) 與var1結合的寄存器是一個earlyclobber,它在gcc尚未訪問完畢輸入運算元之前就被賦值,但是程式中沒有指出這一點。

錯誤a)是我們故意犯的,只是為了說明earlyclobber的含義。 錯誤b)的發生就在於程式沒有理解earlyclobber。

我們先看看這個程式的結果是什麼:

/* FIXME: 錯誤程式不一定產生錯誤結果,有時候需要打開-O2等優化開關才會出錯 */
$ gcc -m32 -O2 wrong.c
$ ./a.out
var1 is 20

可以看到,經過inline asm之後,var1的值變成了20,而不是我們期望的10。 為什麼會發生這樣的結果呢? 我們看看使用 -S和-fverbose-asm 編譯後*.s檔中的相關代碼:

movl $5, -12(%ebp) /, var1
movl $15, -8(%ebp) /, var2
movl -8(%ebp), %eax / var2, tmp62
/APP
/ 7 "wrong.c" 1
movl $10, %eax / tmp61
movl $20, %eax / here is an error, tmp62
/NO_APP
movl %eax, -12(%ebp) / tmp61, var1


從這段彙編代碼中可以看出,gcc讓var1和var2都結合到了eax寄存器中。 這造成第9行var1被寫之後,在第10行中由於var2的
被訪問而再次被寫--於是得到了錯誤結果。

正確的程式是:

$ cat correct.c
#include

int main(void)
{
int var1 = 5, var2 = 15;

asm volatile
(
"movl $10, %0\n\t"
"movl $20, %1"
: "=&r" (var1)
: "r" (var2)
);

printf("var1 is %d\n", var1);
return 0;
}

編譯、運行:
$ gcc -m32 correct.c
$ ./a.out
var1 is 10

正是我們期待的結果。 這是因為,輸出部的約束部分加上了earlyclobber修飾符:&。 這樣,gcc就會保證:
a) 不會把該運算元和任何一個輸入運算元結合到一起;
b) 嚴格檢查,該運算元必須和寄存器結合,而 *不是* 任何記憶體位置

我們看看上面這個正確程式用-S -fverbose-asm編譯之後的*.s檔中的相關代碼:

movl $5, -12(%ebp) /, var1
movl $15, -8(%ebp) /, var2
movl -8(%ebp), %eax / var2, tmp62
/APP
/ 7 "correct.c" 1
movl $10, %edx /
movl $20, %eax / tmp62
/NO_APP
movl %edx, %eax /, tmp61
movl %eax, -12(%ebp) / tmp61, var1

可以看出,gcc為var1結合了edx,為var2結合了eax。
這正是因為我們聲明var1是一個earlyclobber,
從而避免了gcc的錯誤行為。

沒有留言:

張貼留言