前提: 需要你有一定的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什麼的試試。