一天快速入門x86avrarm彙編
前提: 需要你有一定的c/c++經驗, 大概知道一些棧幀、函式呼叫約定之類基礎知識。
每次學一種新語言, 最麻煩的就是折騰環境。 好在有gcc就夠了。 現在新建一個23。c, 內容很簡單如下, 編譯執行會輸出一個1。
int
main
(
void
)
{
printf
(
“%d
\n
”
,
1
);
return
0
;
}
然後執行gcc -masm=intel -Os -S 23。c, 會發現多了一個23。s, 內容是什麼呢, 看看:
。
file
“23。c”
。
intel_syntax
noprefix
。
def
___main
;
。
scl
2
;
。
type
32
;
。
endef
。
section
。
rdata
,
“dr”
LC0
:
。
ascii
“%d
\12\0
”
。
section
。
text
。
unlikely
,
“x”
LCOLDB1
:
。
section
。
text
。
startup
,
“x”
LHOTB1
:
。
globl
_main
。
def
_main
;
。
scl
2
;
。
type
32
;
。
endef
_main
:
push
ebp
mov
ebp
,
esp
and
esp
,
-
16
sub
esp
,
16
call
___main
mov
DWORD
PTR
[
esp
+
4
],
1
mov
DWORD
PTR
[
esp
],
OFFSET
FLAT
:
LC0
call
_printf
xor
eax
,
eax
leave
ret
。
section
。
text
。
unlikely
,
“x”
LCOLDE1
:
。
section
。
text
。
startup
,
“x”
LHOTE1
:
。
ident
“GCC: (tdm-1) 5。1。0”
。
def
_printf
;
。
scl
2
;
。
type
32
;
。
endef
命令列gcc 23。s && a, 會怎麼樣呢? 也是輸出一個1, 和上面的。c完全一樣。
現在看看這個彙編原始檔裡哪些是必須的, 哪些可以刪掉。 首先找到_main, 這是程式入口點, 往後找直到ret, 這是結束。 然後把之後的內容都刪掉, 編譯執行, 正常。
同樣辦法, 刪前面的, 可以發現LC0之前。intel_syntax這一行是必須保留的, 其他可以刪掉。 這是因為gcc預設的彙編格式是at&t, 和我們一般用的intel彙編不太一樣。 繼續刪, 最終得到下面這個:
。
intel_syntax
noprefix
LC0
:
。
ascii
“%d
\12\0
”
。
globl
_main
_main
:
push
ebp
mov
ebp
,
esp
sub
esp
,
16
mov
DWORD
PTR
[
esp
+
4
],
1
mov
DWORD
PTR
[
esp
],
OFFSET
FLAT
:
LC0
call
_printf
xor
eax
,
eax
leave
ret
到這裡基本不能再刪了。 現在可以分析一下每一行是幹啥的:
首先是LC0, 後面有個冒號, 這是個標號, 標號後面內容是什麼呢? 明顯是前面printf的引數“%d\n”, 多了個\0, 因為在c語言裡的字串常量已經隱含了末尾的\0, 這裡需要明確寫出來。 之後是聲明瞭全域性標號_main。 再後面就是正文了。
前三行是建立棧幀, 把ebp暫存器壓棧, 這是上一級的棧底; 然後把esp複製到ebp, 再把esp減去16, 於是建立了16位元組的棧空間。 (刪去了一行and esp, -16, 這是為了讓esp按16位元組對齊。) 這三行在函式的開頭基本是必不可少的, 除了這個16你可以根據自己的需要增加或者減少。
然後是為函式呼叫作準備了, 把LC0的地址放在esp指向的記憶體位置, 把常量1放在esp+4的位置。 之後就可以呼叫printf函數了。 最後面的xor eax, eax是把eax暫存器清零, 比mov eax, 0稍微快一些。
leave則相當於前三行的反過程, 把ebp的內容放回esp, 再把ebp出棧, 於是恢復到呼叫函式之前的狀態了。 最後ret返回。
有了這些, 就可以自由練習了, 保留建立棧幀這三行以及 mov DWORD PTR [esp], OFFSET FLAT:LC0 , 之後隨便你怎麼折騰, 只要最後一條mov把要輸出的暫存器放到esp+4位置, 就可以呼叫_printf看結果了。
好了, 學完了mov, lea, 加減乘除, 各種跳轉, 可以學習迴圈了, 可能會遇到點問題。 假如你的迴圈是這樣寫的, 迴圈輸出0到99:
mov
eax
,
0
loop
:
mov
DWORD
PTR
[
esp
+
4
],
eax
call
_printf
inc
eax
cmp
eax
,
100
jne
loop
首先eax寫0, 把eax寫到esp+4位置, 調printf輸出。 然後eax++, 和100比較, 如果不相等就跳回loop位置。 編譯執行, 怎麼死迴圈了? 查一下呼叫約定, 原來eax是前面printf的返回值。 再查, 原來eax, ecx和edx都不保證在函式呼叫後不變。 那隻能用ebx了, 再試, 果然可以了。
如果連ebx也在迴圈體裡用上了呢? 那隻能在用之前先壓棧, 最後判斷迴圈結束時再出棧了。 當然, 在x86允許你直接給記憶體地址寫0, ++, 和100比較, 相對自由得多, risc上可不能這麼玩。
————現在換成AVR平臺——————
隨便找一塊AVR開發板, 有串列埠就行。 調好串列埠printf輸出, 然後用同樣辦法, avr-gcc -mmcu=atmega328p -Os -S 23。c, 得到的23。s就是avr彙編了。 中間的-mmcu=atmega328p這個按你的avr實際型號來定。 前面建立棧幀和後面收尾操作不太一樣, 建立棧幀:
push r29
push r28
in r28,__SP_L__
in r29,__SP_H__
sbiw r28, 12
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
把原來的棧底位置壓棧, 這是一樣的。 然後把SP讀到r28和r29暫存器, avr的暫存器只有8位, 所以16位的SP需要用兩個暫存器儲存。 之後一條sbiw指令可以給r28和r29組成的16位數-12。 狀態暫存器SREG也需要儲存, 清中斷, 再寫回SP和SREG。
棧恢復原樣: 給r28和r29組成的16位數加12, 儲存SREG, 然後再寫回SREG和SP, 最後r28和r29出棧。 為什麼要增加儲存和恢復SREG的動作呢? 寫回SP的動作不是原子操作, 需要清中斷以免被打斷。
adiw r28,12
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
pop r28
pop r29
ret
中間的部分就是自由發揮時間了, 查查指令集, 把能試的指令都試試, 記得呼叫函式時傳參和返回值都是用以r24開始的暫存器, 可能需要把2個或4個暫存器拼成16位或32位來用。 其他有些指令只是寫法不同, 比如jne變成了brne。
把得到的。s檔案加入到你的工程, 編譯, 下載, 連上串列埠, 可以看到結果了。
————再換成stm32開發板————-
前面的命令變成了arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -S 23。c, 之後的步驟基本一樣。 cortex-m0這裡根據你的stm32型號換成m3或m4。 迴圈輸出0到10大概是這樣的:
。section 。rodata
。align 2
LC0:
。ascii “%d\n\0”
。text
。align 2
。global test
test:
push {r7, lr}
sub sp, sp, #8
mov r7, sp
# start here
mov r2, #0
loop:
mov r1, r2
ldr r0, =LC0
str r2, [r7, #4]
bl printf
ldr r2, [r7, #4]
add r2, #1
cmp r2, #10
bne loop
# end
mov sp, r7
add sp, sp, #8
@ sp needed
pop {r7, pc}
雖然寫了一大堆, 但是真做起來都很快, 只要你手裡有合適的開發板和工具鏈, 一天時間再多搞幾種也完全夠了。
————再更個STM8的————-
最普通的STM8S003, 淘寶上曾經長期一塊多錢那個(現在也漲到好幾塊了)。
執行cxstm8 -s 23。c即可得到23。s。 迴圈輸出0到9的程式長這樣:
xref
_printf
_test
:
subw
sp
,
#
8
ldw
x
,
#
10
ldw
(
4
,
sp
),
x
ldw
x
,
#
0
loop
:
pushw
x
ldw
x
,
#
L3
call
_printf
popw
x
addw
x
,
#
1
cpw
x
,
(
4
,
sp
)
jrne
loop
addw
sp
,
#
8
ret
xdef
_test
L3
:
dc
。
b
“%d”
,
10
,
0
cisc, 和avr比起來是不是簡潔多了? stm8雖然是8位機, 但是x, y和sp暫存器都是16位, 許多指令比如addw, cpw, pushw, ldw也都能直接操作16位, 效能比avr差不了多少。
最後說個搞笑的, 在某論壇看到一個驚悚的貼子: stc16比stm32還快! 他怎麼測試的? 原來是stc16新增了fpu, 他和沒有fpu的stm32f103比浮點運算。 這還沒完, stm32f103的主頻是72M, 這太不公平啦, 把stm32f103降到24M再比。 好吧, 照這麼比, i7也沒你stc快。
有空我再在手裡各個開發板上跑個什麼coremark, dhrystone什麼的試試。