一看二做三总结
分类: 嵌入式
2011-06-17 20:22:26
0 前言
汇编语言,做为最接近硬件实际操作的的语言,是每个嵌入式软件工程师都必须要了解的。所谓嵌入式软件工程师,应该软硬双修,而连接软件与硬件的纽带正是汇编语言。
本文不是讲汇编指令语法,而是专注于C语言是如何变为汇编指令的,通过本文,可以初步掌握C语言于PowePC汇编语言的混合编程,也有利于代码破解工程。
1 伪汇编 1.1 .file标识出文件名,形式为:.file “test.c”。会放在每个文件年的第一行
1.2 .section标识出段的起点,形式为:.section “.text”,该标识直到下一个段标识之间的内容会被放到该标识指定的段内。文件可以有多个同名段,这些段最后会被合为一个段。
1.3 .align对齐符号到指定边界。形式为:.align abs-expr, abs-expr, abs-expr。
第一个参数是对齐要求。对齐要求与系统有关,很多系统认为第一个表达式是字节数。因此,.align 8 就是要8字节对齐。但另一类系统,包括ppc、arm与生成a.out格式的i386,都认为第一个参数是2的阶数。因此,.align 3就是2的3次方对齐,也就是8字节对齐。造成这一差异的原因是gcc需要兼容不同的编译器。
1.4 .ident
应该是用来写一些与编译无关的信息把,格式如下:
.ident “GCC: (Wind River VxWorks G++ 4.1-82) 4.1.2
1.5 全局函数下面是一个完整的全局函数框架
.align 2
.globl Hq_Test ;”globl” means global, so the Hq_Test is a global symbol
.type Hq_Test, @function ; function means Hq_Test is a function
Hq_Test:
stwu 1, -16(1) ; store r1 to [r1]-16 and subtract 16 from r1
stw 31, 12(1) ; store r31 to the stack
mr 31,1 ; move r1(sp) to r31
; add our code here
lwz 11, 0(1) ; load previous sp to r11
lwz 31, -4(11) ; load previous r31
mr 1,11 ; return to the previous sp from r11 to r1
blr ; return from the function
.size Hq_Test, .-Hq_Test ; 含义不清楚
1.6 全局变量
全局变量都会放到“.data”段,一个完整的全局变量定义如下:
.align 2
.type Hq_Var, @object ; object means the Hq_Var is a variable
.size Hq_Var, 4 ; the size of Hq_Var is 4 bytes
Hq_Var:
.long 15 ; the value of Hq_Var is 15
1.7 浮点型全局变量完整定义如下,我么可以发现实际上浮点数也是按照整型数据处理的,也就是说对于最终生成的汇编码来说,立即数全都是整型数据:
.align 2
.type Hq_Var, @object
.size Hq_Var, 4
Hq_Var:
.long 1066275963
2 汇编知识点 2.1 函数的堆栈大小
堆栈大小最小为16 byte,目前powerpc函数堆栈以16字节为最小单元。如果定义一个局部变量,即使是一个字节未初始化变量,堆栈大小也会变成48字节。
2.2 各种C语言变量对应的汇编指令
C代码 |
powerpc汇编指令 |
说明 |
char tt; |
无,但可能会影响堆栈 |
定义时如果不赋初值不会生成汇编指令 |
char tt=0x12; |
li 0, 0x12 stb 0, 8(31) |
b表示byte |
unsigned char tt=0x12; |
同上 |
说明有符号与无符号在指令上没有区别 |
short tt = 0x1234; |
li 0, 0x1234 sth 0, 8(31) |
h表示half word,我们的powerpc是32位的,因此字长为32位,4个byte |
int tt = 0x1234; |
li 0, 0x1234 stw 0, 8(31) |
w表示word,与下面那句相比,该赋值指令明显是经过了优化。这主要是因为编译器会根据立即数的长度判断是否可以一次移入寄存器中。 |
int tt = 0x12345678; |
lis 0, 0x1234 ori 0, 0, 0x5678 stw 0, 8(31) |
lis表示移位赋值,通常在指令后加s就表示移位,移位方式是左移16位,地位补零。之所以把一个赋值语句分为两句汇编指令是因为powepc的指令集无法操作32为的操作数,因此所有超过16位的操作数都分为两个16位的操作数。 |
float tt = 1.2 |
.section .rodata .align 2 .LC0: .long 1067030938 …………….. lis 9, .LC0@ha lfs 0, .LC0@l(9) stfs 0, 8(31) |
浮点立即数都会被首先作为整型数据放到rodata段中,(不过此处为何.rodata没有加引号呢?),而整型立即数会被直接传给寄存器,而不会先存起来。另一个区别是浮点寄存器用到了浮点运算符,以lfs为例,f表示浮点运算,s在浮点运算中表示单精度,这与整型指令中不同。 |
double tt = 1.2 |
.section .rodata .align 3 .LC0: .long 1072902963 .long 858993459 …………….. lis 9,.LC0@ha lfd 0,.LC0@l(9) stfd 0,8(31) |
fd表示双精度浮点型。 8字节对齐
|
char Hq_Var[5] = {11,21,33,57,93}; |
.section .rodata .align 2 .type C$0$1311, @object .size C$0$1311, 5 C$0$1311: .byte 11 .byte 21 .byte 31 .byte 47 .byte 93 …………….. lis 9,C$0$1311@ha la 9,C$0$1311@l(9) lwz 0,0(9) lbz 9,4(9) stw 0,8(31) stb 9,12(31) |
这时字符型数组的定义方式,对于局部数组变量的初始化,我们会把它的初始值放到rodata段,并用一个临时生成的名字表示它。la指令的意思是r9的内容加上偏移量赋给r0 |
|
|
|
2.3 函数调用对应的汇编
C代码 |
汇编指令 |
说明 |
void Hq_Test02(void); void Hq_Test01(void) { char tt=1; Hq_Test02(); } |
Hq_Test01: stwu 1,-48(1) stw 31,44(1) mr 31,1 li 0,1 stb 0,8(31) lwz 11,0(1) lwz 31,-4(11) mr 1,11 blr |
这是一个空白的函数,也是下列一系列函数的基础,我们会逐步改变代码 |
void Hq_Test02(void); void Hq_Test01(void) { char tt=1; Hq_Test02(); } |
Hq_Test01: stwu 1,-48(1) mflr 0 stw 0,52(1) stw 31,44(1) mr 31,1 li 0,1 stb 0,8(31) bl Hq_Test02 lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr |
增加一个最简单的函数调用,该函数没有出参没有入参,可以看到增加了五行代码,除一句用于跳转外,其他四句都用于现场保护(这种最简单的函数只需要保存与恢复lr寄存器) |
|
|
|
void Hq_Test02(char tt); void Hq_Test01(void) { char tt=1; Hq_Test02(tt); } |
Hq_Test01: stwu 1,-48(1) mflr 0 stw 0,52(1) stw 31,44(1) mr 31,1 li 0,1 stb 0,8(31) lbz 0,8(31) rlwinm 0,0,0,0xff mr 3,0 bl Hq_Test02 lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr |
这次增加了一个入参,入参占用寄存器r3到r10,这里有一个入参,所以占用寄存器r3。 lbz会把r31指向的内存的第一个字节移入r0的低字节rlwinm是左移指令,这里的用处是用0xff作掩码。 |
void Hq_Test02(unsigned short tt); void Hq_Test01(void) { unsigned short tt=1; Hq_Test02(tt); } |
Hq_Test01: stwu 1,-48(1) mflr 0 stw 0,52(1) stw 31,44(1) mr 31,1 li 0,1 sth 0,8(31) lhz 0,8(31) rlwinm 0,0,0xffff mr 3,0 bl Hq_Test02 lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr |
本例中的入参由char型变为了int型。可以看到除了有b型改为了h型,其他都是一样的 |
void Hq_Test02(short tt); void Hq_Test01(void) { short tt=1; Hq_Test02(tt); } |
Hq_Test01: stwu 1,-48(1) mflr 0 stw 0,52(1) stw 31,44(1) mr 31,1 li 0,1 sth 0,8(31) lhz 0,8(31) extsh 0,0 mr 3,0 bl Hq_Test02 lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr |
本例中的入参由unsigned short型变为了unsigned short型,char型与int型都不会因符号而不同。但short型与符号有关。extsh用于符号扩展。 |
|
|
|
void Hq_Test02(int tt); void Hq_Test01(void) { int tt=1; Hq_Test02(tt); } |
Hq_Test01: stwu 1,-48(1) mflr 0 stw 0,52(1) stw 31,44(1) mr 31,1 li 0,1 stw 0,8(31) lwz 3,8(31) bl Hq_Test02 lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr |
由于是w指令,与寄存器长度相同,因此不需要掩码。 |
void Hq_Test02(char tt); void Hq_Test01(void) { char tt=1; tt = Hq_Test02(tt); } |
Hq_Test01: stwu 1,-48(1) mflr 0 stw 0,52(1) stw 31,44(1) mr 31,1 li 0,1 stb 0,8(31) lbz 0,8(31) rlwinm 0,0,0,0xff mr 3,0 bl Hq_Test02 mr 0,3 stb 0,8(31) lwz 11,0(1) lwz 0,4(11) mtlr 0 lwz 31,-4(11) mr 1,11 blr |
与没有返回值的char函数的代码比较,返回值会放到r3寄存器并存入栈中 |
|
|
|
2.4 不同数据类型互转对应的汇编
C代码 |
汇编指令 |
说明 |
|
|
|
|
|
|
|
|
|
2.5 for、if等基本语言结构对应的汇编
C代码 |
汇编指令 |
说明 |
int tt=12; if (0) tt = 25; else tt = 41; |
li 0,12 stw 0,8(31) li 0,41 stw 0,8(31) |
对于if 后跟立即数的情况,编译器会根据立即数判断出下面的分支是否可以走到。如果不可能会走到,则不会生成汇编指令;如果一定会走到(条件不为0),则不会有跳转指令
|
int tt=12; if (tt) tt = 25; else tt = 41; |
li 0,12 stw 0,8(31) lwz 0,8(31) cmpwi 7,0,0 beq 7,.L2 li 0,25 stw 0,8(31) b .L5 .L2: li 0,41 stw 0,8(31) .L5: |
这时最简单的跳转指令,对于条件跳转,一般都对应两句汇编指令,比较与跳转。 |
if (tt == 42) tt = 52; else if (tt == 43) tt = 53; else tt = 55; |
li 0,41 stw 0,8(31) lwz 0,8(31) cmpwi 7,0,42 bne 7,.L2 li 0,52 stw 0,8(31) b .L7 .L2: lwz 0,8(31) cmpwi 7,0,43 bne 7,.L5 li 0,53 stw 0,8(31) b .L7 .L5: li 0,55 stw 0,8(31) .L7: |
从这个条件可以看出这种实用else if的情况,相当于是多层if条件的判断。 |
int tt=41; switch (tt) { case 42: tt = 52; case 43: tt = 53; break; default: tt = 55; } |
li 0,41 stw 0,8(31) lwz 0,8(31) stw 0,24(31) lwz 0,24(31) cmpwi 7,0,42 beq 7,.L3 lwz 0,24(31) cmpwi 7,0,43 beq 7,.L4 b .L2 .L3: li 0,52 stw 0,8(31) /* b .L6 */ .L4: li 0,53 stw 0,8(31) b .L6 .L2: li 0,55 stw 0,8(31) .L6: |
switch与if嵌套是不同的。从代码结构看,switch结构更整齐一些。红色与蓝色各对应一个case分支,基本都可以单独去掉而不影响代码结构。绿色对应的是default分支,它与case的区别是它不需要判断,如果所有case分支都没有走到的话,自然会跳到default。由于default是最后一个跳转分支,因此效率自然是比不上前面的case,尤其是case很多时。
|
int tt=41; switch (tt) { case 42: tt = 52; break; case 43: tt = 53; break; default: tt = 55; } |
li 0,41 stw 0,8(31) lwz 0,8(31) stw 0,24(31) lwz 0,24(31) cmpwi 7,0,42 beq 7,.L3 lwz 0,24(31) cmpwi 7,0,43 beq 7,.L4 b .L2 .L3: li 0,52 stw 0,8(31) b .L6 .L4: li 0,53 stw 0,8(31) b .L6 .L2: li 0,55 stw 0,8(31) .L6: |
可以看到比上一个多一个break的结果是语句中多了一个绝对跳转指令b .L6,这也正常,因为break本来就是对应绝对跳转。从汇编角度理解,可以认为switch是用于决定程序会从switch中的哪个case开始执行;只是由于break的加入才使得switch可以实现if else可以实现的功能。 |
int tt=41; do { tt = 42; } while (tt == 43); |
li 0,41 stw 0,8(31) .L2: li 0,42 stw 0,8(31) lwz 0,8(31) cmpwi 7,0,43 beq 7,.L2 |
while与if一样,都是比较指令加跳转指令,唯一的区别是while会跳转到跳转指令之前,从而实现循环 |
int tt=41; while (tt == 43) { tt = 42; } |
li 0,41 stw 0,8(31) b .L3 .L2: li 0,42 stw 0,8(31) .L3: lwz 0,8(31) cmpwi 7,0,43 beq 7,.L2 |
从while do与do while比较可看出,do while下while的内容必然会执行一次,while do则的内容则可能一次也不会执行 |
int tt=41; for (tt=44; tt == 43; tt++) { tt = 42; } |
li 0,41 stw 0,8(31) li 0,44 stw 0,8(31) b .L3 .L2: li 0,42 stw 0,8(31) lwz 9,8(31) addi 0,9,1 stw 0,8(31) .L3: lwz 0,8(31) cmpwi 7,0,43 beq 7,.L2 |
for循环与while 循环是相似的。本例子下,如果没有tt=44与tt++两句,则上面的while do结构产生的代码是一样的。如果加了这两句,则结构如左 |
|
|
|
注:从上面的例程中看,所有的变量必须都放入栈中,如果要实用一个变量,就必须经历下面山歌步骤:
(1) 从栈中取出变量放入寄存器
(2) 使用寄存器中的值
(3) 把寄存器中的值存入栈(如果没有修改寄存器的值则没有这条)