Transcript 運算子

第八章
ARM C語言的使用
DMATEK CO.,LTD
深圳市長高科技有限公司








本章節將介紹ARM C語言程式設計概念,透過對本文的閱讀,希望讀者能瞭解ARM微處
理器支援的C語言的使用方法,我們會搭配反組譯的功能,讓讀者更瞭解ARM是如何來
支援高階語言的運作。
本章的主要內容有:
-
抽象化概念
-
運算子
-
區域變數/全域變數
-
指標運算
-
迴圈/條件判斷
-
傳址呼叫和傳值呼叫
 8-1 抽象化概念:
 當讀者熟讀前面幾章後,應該對ARM有深刻的印象,在指令集您會感覺到ARM的強大
功能,單一指令可以同時完成許多動作,然而使用組合語言來撰寫程式,您一定倍感
辛苦,而特別是靈活運用條件執行﹝Conditional Execution﹞,把每一道指令添增了
16種變化,相信您必須具備孫悟空72變化能力才能順利駕馭。本章將說明如何使用高
階語言來撰寫ARM應用程式。在這之前先讓我們來看看組合語言及C語言抽象化的概
念。
 組合語言層次的抽象化
程式設計師直接以指令來撰寫組合語言的程式,您必須熟悉指令格式、定址方式、
暫存器及記憶體空間等觀念。通常組合言的指令和機器指令是採1對1的關係,在指令
格式中要特別注意條件執行,若能妥善運用不但能增強管線的效能而且程式也變得更
加精簡,程式長度有可能比高階語言還要短。ARM指令集可分成ARM及THUMB兩種
,前者為32位元而後者則為16位元。通常對於要求效能可以使用32位元,但對於空間
及節能等需求時則可以考慮將部份程式用THUMB指令撰寫。
 高階語言層次的抽象化
 高階語言允許程式設計師以跳脫機械層級的思考來撰寫程式。高階語言和機器語言
間已不是1對1的關係,對高階語言的程式設計師而言,不見得要熟悉ARM指令或
暫存器等配置。所以有很多高階語言程式設計師並不見得對ARM機器有很深的瞭
解。然而建議讀者要清楚瞭解ARM的硬體結構,在撰寫C語言程式時,能多考慮到
ARM機器的特質,如此才能事半功倍。例如:過長的迴圈將會使快取記憶體(
Cache)無法發揮它的長才。
 8-2 運算子:

1.
2.
3.
4.
5.
6.
7.
8.
C語言提供強大的運算子來處理運算工作,運算子可分成一元運算子
﹝Unary Operators﹞、二元運算子﹝Binary Operators﹞、及三元運算子
﹝Ternary Operators﹞,依其功能可將運算子可分成下列:
算術運算子﹝Arithmetic Operators﹞
關係運算子﹝Relational Operators﹞
邏輯運算子﹝Logical Operators﹞
指定運算子﹝Assignment Operators﹞
增減運算子﹝Increments and Decrement Operators﹞
條件運算子﹝Conditional Operators﹞
位元運算子﹝Bitwise Operators﹞
特殊運算子﹝Special Operators﹞


8-2.1 算術運算子
算術運算子可分成:加法、減法、乘法、除法、及取餘數,分別用+、-、*、/、及%來表示
。詳如下表:
運算子
意思
範例
+
加
X+1
-
減
X-1
*
乘
X*Y
/
除
X/5
%
取餘數
X%4
 以下為運算子的範例:
int main()
{
int a,b,c;
a=1;
b=2;
c=a+b;
return 0;
}
 其反組譯程式如下:
1
int main()
2
{
3
int a,b,c;
4
a=1;
main[0xe3a01001] * mov
r1,#1
5
b=2;
000080ac
[0xe3a02002]
6
c=a+b;
000080b0
[0xe0813002]
7
return 0;
000080b4
[0xe3a00000]
8
}
000080b8
[0xe1a0f00e]
mov
r2,#2
add
r3,r1,r2
mov
r0,#0
mov
pc,r14
 接下來我們來觀察,ARM對於除法的支援情形,以下是除法範例的反組譯程式列
表:
3
int a,b,c;
4
a=6;
000080ac
[0xe3a04006] mov
r4,#6
5
b=2;
000080b0
[0xe3a05002] mov
r5,#2
6
c=a/b;
000080b4
[0xe1a01004] mov
r1,r4
000080b8
[0xe1a00005] mov
r0,r5
000080bc
[0xeb000004] bl
__rt_sdiv
000080c0
[0xe1a06000] mov
r6,r0
 您應該可以發現ARM並未提供除法指令而由呼叫__rt_sdiv副程式取代,我們再來
看看取餘數的反組譯程式列表:
3
int a,b,c;
4
a=6;
000080ac
[0xe3a04006] mov
r4,#6
5
b=2;
000080b0
[0xe3a05002] mov
r5,#2
6
c=a%b;
000080b4
[0xe1a01004] mov
r1,r4
000080b8
[0xe1a00005] mov
r0,r5
000080bc
[0xeb000004] bl
__rt_sdiv
000080c0
[0xe1a06001] mov
r6,r1


8-2.2 關係運算子
關係運算子有小於、小於等於、大於、大於等於、等於、及不等於,用<、<=、>、
>=、==、及!=來表示。詳如下表:
運算子
意思
範例
<
小於
X<1
<=
小於等於
X<=1
>
大於
X>Y
>=
大於等於
X>=Y
==
等於
X==1
!=
不等於
X!=1
 小於關係運算子的範例如下:
3
int a,b,c;
4
a=6;
main
[0xe3a01006] * mov
r1,#6
5
b=2;
000080ac [0xe3a02002] mov
r2,#2
c=a<b;
000080b0[0xe1510002] cmp
r1,r2
000080b4[0xaa000001] bge
0x80c0 ; (main + 0x18)
000080b8[0xe3a00001] mov
r0,#1
000080bc [0xea000000] b
0x80c4 ; (main + 0x1c)
000080c0 [0xe3a00000] mov
r0,#0
000080c4 [0xe1a03000] mov
r3,r0
由上面範例可知小於關係運算子是利用cmp指令來設計,並利用分支指令b及
其條件執行參數命令ge配合來達到將運算結果真﹝1﹞或假﹝0﹞利用將r0暫存再
指定到c變數﹝r3﹞來儲存。


8-2.3 邏輯運算子
邏輯運算子有且、或、及非分用&&、||、及!來表示。詳如下表:
運算子
意思
範例
&&
邏輯 “且” AND
X && Y
||
邏輯 “或” OR
X || Y
!
邏輯 “非” NOT
!X
 “且”邏輯運算子的範例如下:
3
int a,b,c;
4
a=6;
main
[0xe3a01006] * mov
r1,#6
5
b=2;
000080ac [0xe3a02002] mov
r2,#2
6
c=a&&b;
000080b0[0xe3510000] cmp
r1,#0
000080b4[0x0a000003] beq
0x80c8 ; (main + 0x20)
000080b8[0xe3520000] cmp
r2,#0
000080bc [0x0a000001] beq
0x80c8 ; (main + 0x20)
000080c0 [0xe3a00001] mov
r0,#1
000080c4 [0xea000000] b
0x80cc ; (main + 0x24)
000080c8 [0xe3a00000] mov
r0,#0
000080cc [0xe1a03000] mov
r3,r0
 在上面範例您有發現“且”具備捷徑﹝Shortcut﹞的功能,當變數a為假時則可以不
需要考慮到變數b,此一作用我們稱為捷徑,若變數a為真時才需要測試變數b是否
為真,當變數a及b都為真時,“且”關係運算子才會傳回真﹝0﹞,否則傳回假
﹝0﹞。


8-2.4 指定運算子
指定運算子是用等號﹝=﹞來表示,要特別注意它具備指定的功能而非等於,它會把等
號右側的常數值或變數值指定到等號左手邊的變數上。指定即為拷貝因此當指定運算子執行後
,左側變數會等於右側計算後的結果,由於前面已使用過,在此不詳述。


8-2.5 增減運算子
增減運算子就是加1或減1分別使用++和—來表示。
運算子
意思
範例
++
加一
X++或++X
--
減一
X--或--X
 其範例反組譯程式列表如下:
3
int a,b;
4
b=a++;
main
[0xe1a02001] * mov
000080ac [0xe2811001] add
r1,r1,#1
5
b=++a;
000080b0[0xe2810001] add
r0,r1,#1
000080b4[0xe1a01000] mov
r1,r0
000080b8[0xe1a02000] mov
r2,r0
6
b=a--;
000080bc [0xe1a02001] mov
r2,r1
000080c0 [0xe2411001] sub
r1,r1,#1
7
b=--a;
000080c4 [0xe2410001] sub
r0,r1,#1
000080c8 [0xe1a01000] mov
r1,r0
000080cc [0xe1a02000] mov
r2,r0
r2,r1
 8-2.6 條件運算子
 條件運算子是一個三元運算子,它以?和:來組成,其語法如下:
exp1 ? exp2 : exp3
 當exp1條件運算為真時,則傳回exp2運算值否則傳exp3運算值,其範例如下:
a = 10;
b = 15;
x = (a > b) ? a : b
 在上面範例當變數a大於b時,則將變數x內容指定成a否則指定成b。其反組譯程
式碼列表如下:
3
int a,b,c;
4
a=1;
main
[0xe3a01001] * mov
r1,#1
5
b=2;
000080ac [0xe3a02002] mov
r2,#2
6
c=(a>b)?a:b;
000080b0[0xe1510002] cmp
r1,r2
000080b4[0xda000001] ble
0x80c0 ; (main + 0x18)
000080b8[0xe1a00001] mov
r0,r1
000080bc [0xea000000] b
0x80c4 ; (main + 0x1c)
000080c0 [0xe1a00002] mov
r0,r2
000080c4 [0xe1a03000] mov
r3,r0


8-2.7 位元運算子
位元運算子即為位元運算指令,有且、或、互斥、非、向右位移、及向左位移,用&、|、^
、~、>>、及<<來表示。詳如下表:
運算子
意思
範例
&
位元邏輯 “且” AND
X AND Y
|
位元邏輯 “或” OR
X OR Y
^
位元邏輯 “互斥” XOR
X^Y
~
位元邏輯 “非” NOT
~X
>>
向右位移
X >> 2
<<
向左位移
X << 1
 圖 8-1 顯示且位元運算子的運算結果,在圖中您可以發現且位元運算子﹝&﹞,
使用and指令來組譯。當變數a和b分別指定為3和2時,再執行且位元運算式
c=a&b後,變數c的結果為2,有興趣的讀者不妨把這些數值用二進位來表示,
然後再執行且位元運算子﹝&﹞後,就可以得知為何是2。
圖8-1 且位元運算子執行結果
 8-2.8 特殊運算子
 最後我們來看特殊運算子,分列如下:
 逗號運算子(comma operator)
value = (x = 10, y = 5, x + y);
 變數大小運算子(size of operator) sizeof
m = sizeof (sum);
 指標運算子(pointer operators) (& and *)
 成員選取運算子(member selection operators) (. and ->)
 8-3 全域變數和區域變數:

變數主要是用來讓程式設計者暫時存放數值的地方,當您有需要時可以將它取
出或修改它。在C語言中變數可分成兩大類:
 全域變數
 宣告在程式開頭處即為函式外面,其範圍涵蓋所有函式。通常使用記憶體空間
來存放。
 區域變數
 宣告在函式內部,都以是暫存器或堆疊來存放,其使用範圍僅限於函式內部。
 以下為全域變數和區域變數的程式範例:
//採用全域變數
int a, b, c;
int main()
{
a=1; b=1;
c = a+b;
return 0;
}
//採用區域變數
int main()
{
int a, b, c;
a=1; b=1;
c = a+b;
return 0;
}
 上面範例看起來程式長度一樣,但我們利用反組譯功能,您會發現多使用區域
變數會提高程式的效能,其程式列表如下圖:
圖8-2 全域變數及區域變數效能比較
 8-4 指標變數:

指標變數簡單來說是一種指位器,該變數儲存不是數值內容而是位址,我們可以
使用*來宣告,使用如下:
int *p;
 在計算指標的大小時,可分成編譯階段得知或執行階段得知,編譯階段得知範例如
下:
int *p;
p=p+1;
 我們得知每次增加4位元組,因為指標的資料型態為整數佔四個位元組。但有些情
形不易得知,例如:
int *p;
int i=4;
p=p+i;
 在此情形則需要執行階段才能獲知,則編譯器會使用ADD指令來處理。C語言最迷
人地方在於它擁有指標功能,但也是它可怕的地方。圖8-3展示其可怕之處,從圖
中您會得知指標居然指到位址0,而且我們把它改成1。從此您可以得知指標能修
改記憶體的內容,但必須要非常小心,否則您會把重要的程式或資料給修改,造成
不可預測的結果。
圖8-3 危險指標
 圖8-4說明將變數指向變數,您會發現我們可以安全地變更p指標所指變數的內容
,在圖中a變數為0xc000而p變數則為0xc004。
圖8-4 指標一定要指向變數
 8-5 條件敍述:

C語言條件敍述有if和switch兩種,if又可搭配else使用。if範例如下:
if (a>b) c=a; else c=b;
 上述程式說明當a>b條件成立時c變數指定為變數a的內容否則指定為變數b。當上面
範例組譯成組合語言時,可以反組譯成下面:
CMP
r0,r1
;if(a>b) …
MOVGT
r2,r0
;..c=a..
MOVLE
r2,r1
;…else c=b..
 以下程式碼是C與組合語言混合寫法,詳細語法請參照8-8節。首先使用AXD模擬針
對if….else的C語言程式碼如下,並進行C的反組譯如下圖8-5灰色字所示,執行結果
,由於A小於B所以執行A加B的動作,結果在Global Variable 的值A等於9,B等於5
。
int a=4,b=5;
int Main(void)
{
if (a>b) L
a=a-b;
else
a=a+b;
return 0;
圖 8-5 if….else範例程式示意圖
接下來我們來看另一個敍述switch,其語法如下:
switch (條件表示式) {
case 常數1: 敍述區塊1;break;
case常數2: 敍述區塊2;break;
…
case常數N: 敍述區塊N;break;
default: 敍述區塊d;break;
}
ARM在支援switch指令採用,有時會採取跳躍表格方式處理,利用一表格來儲
存各常數值的目的地,其指令樣板如下:
;r0 包含條件表示式的數值
ADR
r1,JUMPTABLE
;取得跳躍表格的基值
CMP
r0,#TABLEMAX
;表格最大值
LDRLS
pc,[r1,r0,LSL#2]
;改變PC值來進行跳躍
B
Exit
L1: ..
B
Exit
..
LN ..
Exit..
 以下程式碼是C與組合語言混合寫法,詳細語法請參照8-8節。首先使用AXD
模擬針對switch…case 的C語言程式碼如下,並進行C的反組譯如下圖8-6灰色
字所示,執行結果,由於B減A等於2,所以會執行case 2這段程式碼,B減A的
動作,結果如下圖8-6在Global Variable 的值A等於2,B等於5。
int a=3,b=5;
int Main(void)
{
*9* switch (b-a)
{
case 1:a=a+b;break;
case 2:a=b-a;break;
}
return 0;
}
圖 8-6 switch….case範例程式示意圖
 8-6 迴圈敍述:

C語言支援三種迴圈敍述有for、while、do..while。for範例如下:
for (i=0;i<10;i++)
{
a[i]=0;
}

由於上面範例僅將陣列10個元素全數設定為零,因此在組譯成組合語言時,可
以利用在迴圈外增加指令MOV r0,#0,來加速指令的執行,其反組譯為:
MOV
r1,#0
ADR
r2,#a[0]
MOV
r0,#0
;i=0
LOOP
CMP
r0,#10
BGE
Exit
STR
r1, [r2,r0,LSL #2]
ADD
r0,r0,#1
B
LOOP
Exit
 以下程式碼是C與組合語言混合寫法,詳細語法請參照8-8節。首先使用AXD模擬
針對for …next的C語言程式碼如下,並進行C的反組譯如下圖8-7灰色字所示,執
行結果,由於I小5,所以會執行A=A+I這段程式碼5次,執行過程0+1+2+3+4,
所以A會一直累加I的值,結果如下圖8-7在Global Variable的值A等於10,I等於5
。
int i,a;
int Main(void)
{
a=0;
for (i=0;i<5;i++)
{
a=a+i;
}
return 0;
}
圖 8-7 for…next範例程式示意圖
 對於while迴圈則較簡單,可以使用下列組合語言程式來表示:
LOOP
 ;插入條件判斷的指定
BEQ
exit
迴圈本體
B
LOOP
Exit
 以下程式碼是C與組合語言混合寫法,詳細語法請參照8-8節。首先使用AXD模擬針
對while的C語言程式碼如下並進行C的反組譯如下圖8-8灰色字所示,執行結果,由
於I小5,所以會執行while這段程式碼會詢問6次,執行結果為1+2+3+4+5,而詢問第
六次時,就會跳開while迴圈,而A會一直累加I的值,所以結果如下圖在Global
Variable的值A等於15,I等於5。
int i,a;
int Main(void)
{
a=0,i=0;
while (i<5)
{
i=i+1;
a=a+i;
}
return 0;
}
圖 8-8 while…範例程式示意圖
 對於do..while迴圈則較簡單,可以使用下列組合語言程式來表示:
LOOP
;插入迴圈本體程式
;插入條件判斷的指定
BNE
LOOP
 以下程式碼是C與組合語言混合寫法,詳細語法請參照8-8節。首先使用AXD模擬
針對do….while的C語言程式碼如下,並進行C的反組譯如下圖8-9灰色字所示,執
行結果,由於I小5,所以會執行do…while這段程式碼只會執行5次,執行結果為
1+2+3+4+5,而詢問第5次時,就會直接跳開do...while迴圈,而A會一直累加I的
值,所以結果如下圖8-9在Global Variable的值A等於15,I等於5。
int i,a;
int Main(void)
{
a=0,i=0;
do
{
i=i+1;
a=a+i;
}
while (i<5) ;
return 0;
}
圖 8-9 do….while範例程式示意圖
 8-7 程式呼叫標準:
 ARM支援一套程式呼叫的標準(ARM Procedure Call Standard),簡稱為
APCS。APCS 定義下列內容:
 一般暫存器的特殊使用
 堆疊指標的使用
 堆疊的資料格式
 函式參數傳遞和傳值格式
 支援ARM共用函式庫
 其參數使用暫存器如下表所示:
 在使用函式時,經常會利用參數來傳遞數值,最常見的就是傳值呼叫(Call by
Vakue)及傳址呼叫(Call by Address),其範例如圖8-10和8-11所示。讀者請特
別注意兩者在使用上的差異,它們之間的差別在於是否使用指標。
圖8-10 傳值呼叫範例執行結果
圖8-11 傳址呼叫範例執行結果
 上面兩個範例,相信您已發現在Console的視窗,其執行結果不同,傳值呼叫不會
改變主程式的變數值然而傳址呼叫會。接下來我們來看看函式指標如圖8-7所示,
原來指標不是只用來指向資料變數,也可以用來指向函數本體。您可經過下列三
步驟,即可輕輕鬆鬆地使用函式指標,我們以指向swap函式當成範例。
 步驟一、宣告函式指標。
void *(pFn) (int a, int b);
 步驟二、指定函式。
pFn = &swap;
 步驟三、使用函式指標來呼叫函式。
*(pFn) (&a, &b);
圖8-12 函式指標範例執行結果
 上面函式的反組譯程式列表如下:
int main()
{
main
[0xe92d400e] * stmfd r13!,{r1-r3,r14}
int a=1,b=2;
000080c0 [0xe3a01001] mov
r1,#1
000080c4 [0xe58d1008] str
r1,[r13,#8]
000080c8 [0xe3a02002] mov
r2,#2
000080cc [0xe58d2004] str
r2,[r13,#4]
void (*pFn)(int *a, int *b);
pFn=&swap;
printf ("a=%d, b=%d\n",a,b);
000080d0[0xe28f0024] add
r0,pc,#0x24 ; #0x80fc
000080d4[0xeb00000d] bl
_printf
(*pFn)(&a,&b);
000080d8[0xe28d1004] add
r1,r13,#4
000080dc [0xe28d0008] add
r0,r13,#8
000080e0[0xebfffff0] bl
swap
printf ("a=%d, b=%d\n",a,b);
000080e4[0xe28f0010] add
r0,pc,#0x10 ; #0x80fc
000080e8[0xe59d1008] ldr
r1,[r13,#8]
000080ec [0xe59d2004] ldr
r2,[r13,#4]
000080f0 [0xeb000006] bl
_printf
return 0;
000080f4 [0xe3a00000] mov
r0,#0
}
000080f8 [0xe8bd800e] ldmfd r13!,{r1-r3,pc}
000080fc [0x64253d61] dcd
0x64253d61 a=%d
00008100[0x3d62202c] dcd
0x3d62202c , b=
8104[0x000a6425] dcd
0x000a6425 %d..

雖然函式指標是C語言的特色,而且具備強大的功能,但當您看過反組譯程式後
,您會發現在這個強大功能的背後,原來是使用ADD以及BL指令來完成,ADD指
令用來取得傳址的位址,而BL指令則是用來呼叫副程式。若能善用指令的相互搭配
就能創造迷人的功能,利用ADD及BL指令來設計函式指標的功能,就是一個明顯的
例子。
 8-8 C與組合語言的混合撰寫設計:
 在應用系統的程式設計中,若所有的編寫程式任務均用組合語言來完成,其工作量
是可想而知的,同時,不利於系統升級或應用軟體移植,事實上,ARM體系結構支
援C/C+以及與組合語言的混合編寫程式,在一個完整的程式設計的中,除了初始化
部分用組合語言完成以外,其主要的編寫程式任務一般都用C 完成。
 組合語言與C的混合編寫程式通常有以下幾種方式:
-
在C程式碼中嵌入編譯指令。
-
在組合語言程式和C的程式之間進行變數的互傳。
-
組合語言程式、C程式間的相互呼叫。
在以上的幾種混合編寫程式技術中,必須遵守一定的呼叫規則,如實體位址的
使用、參數的傳遞等,這對於初學者來說,無疑顯得過於煩瑣。在實際的編寫程式
應用中,使用較多的方式是:程式的初始化部分用組合語言完成,然後用C完成主要
的編寫程式任務,程式在執行時首先完成初始化過程,然後跳躍到C程式碼中,組合
語言程式和C程式之間一般沒有參數的傳遞,也沒有頻繁的相互呼叫,因此,整個程
式的結構顯得相對簡單,容易理解。以下是一個組合結構程式的基本範例:
IMPORT Main
;通知編譯器該標號為一個外部標號
AREA Init, CODE, READONLY ;定義一個程式碼段
ENTRY
;定義程式的入口點
LDR
R0, =0x3FF0000
;初始化系統配置暫存器,
LDR
R1, =0xE7FFFF80
STR
R1,[R0]
LDR
SP, =0x3FE1000
;初始化用戶堆疊
BL Main
;跳躍到Main()函數處的C程式碼執行
END
;標識組合語言程式的結束
 以上的程式段完成一些簡單的初始化,然後跳躍到Main()函數所標識的C程式碼
處執行主要的任務,此處的Main僅為一個標號,也可使用其他名稱,與C語言程式
中的main()函數沒有關係。
void Main(void)
{
int i;
*((volatile unsigned long *) 0x3ff5000) = 0x0000000f;
while(1)
{
*((volatile unsigned long *) 0x3ff5008) = 0x00000001;
for(i=0; i<0x7fFFF; i++);
*((volatile unsigned long *) 0x3ff5008) = 0x00000002;
for(i=0; i<0x7FFFF; i++);
}
}







8-9 問題與討論:
一、請說明組合語言和C語言在抽象化概論上差異。
二、C語言支援那些問算子。
三、說明傳值呼叫和傳址呼叫。
四、說明全域變數和區域變數的使用方式及其效能比較。
五、請列出for、if反組譯成組合語言的指令。
六、請撰寫一支程式可求1至100整數的總合,並利用ADS把該程式反組譯成組合
語言。
 七、請利用ADD指來撰寫一個兩數相加的副程式,並使用C誩言宣告兩個整數變數
,並呼叫該組合語言的副程式來進行加總。