零.前言
因为明天就要考试了,但是我自己的我自己的51单片机还停留在C语言开发的层面,但是考试要求用汇编,所以花一点儿时间来学习"编译自动机"(其实是自己把常见的关键字if
、for
、[]
给弄成汇编版的,便于写代码)。
本身由于以前接触过逆向,所以有一点儿汇编基础,以及能熟练的使用C语言给51单片机编程,所以这篇文章的重点,是在C转汇编
上,且为了应付考试,只选择比较好记
的方式,而不讲究代码效率。
一.结构
在汇编中,由于代码是按地址顺序执行的,所以为了方便,我习惯将每一个功能,比如if
、for
都给封装
成一个函数,这样逻辑清晰又好记。
我有一个习惯,累加器不用来进行数据存储,仅用于运算和临时变量,不做储存;储存的话,习惯用工作寄存器Rn去储存。
二.判断语句
在C语言中,我们的if是这样的:
if (condition)
{
}
else
{
}
而CJNE
是一个适用范围比较大的指令,所以我们就以它为基础,写一个判断语句:
JUDGE: CJNE R0, #01H, JFALSE
MOV R1, #0FFH
RET
JFALSE: MOV R1, #00H
RET
这条命令就是判断R0是否为0x01
,若是,则给R1赋值为0xFF
,若不是,则赋值0
。等同:
if (R0 == 0x01)
{
R1 = 0XFF;
return;
}
else
{
R1 = 0x00;
return;
}
举个例子,这是一个判断R0的值,并点灯的程序:
灯亮:
灯不亮:
三.循环语句
我们对一个C语言的分析,我将其写成比较好看的形式:
R2 = 5;
while(--R2 > 0)
{
//express
}
这是一个很基础的循环,那么我们可以用DJNZ
来模拟这个功能:
REPEATINIT: MOV R2, #05H
REPEATBEGIN:DJNZ R2, REPEATEXPRE
RET
REPEATEXPRE:INC A
SJMP REPEATBEGIN
这段代码会执行++A
4次,因为是先减。
抓换成C语言代码为:
R2 = 5; // REPEATINIT
while(--R2 > 0) // REPEATBEGIN
{
++A; // REPEATEXPRE
}
return; // RET
这是一个每次让P2+4的代码:
当然还有一种死循环语句:
while(--R5);
这种代码可以用以下代码代替:
DJNZ R5, $
四.下标[]
在汇编中,其实一段数的地址本身就是连续的可以用[基址]+偏移指针
的方式来模拟数组,所以一般来说,我们在这里面多用这种方式来查表:
基址使用DPTR来代替,比如:
MOV DPTR,#TAB
这样可以通过MOVC A,@A+DPTR (A=0、1、2……len)
来将TAB中的内容赋值给A。
举例:对P2循环赋值数组DB
中的内容(就不写延时了,通过调试步进查看代码效果)
也就是这样亮:
●●●●●●●● FFH
○●●●●●●○ 7EH
○○●●●●○○ 7CH
○○○●●○○○ 18H
○○○○○○○○ 00H
但是我们循环是从高减到0,所以索引因该倒着排。
当然,可以用数再减此时的循环变量的值可以得到正向索引。
完整代码:
ORG 0000H
MOV DPTR, #TAB
LJMP MAIN
ORG 0100H
MAIN: LCALL REPEATINIT
SJMP MAIN
REPEATINIT: MOV R0, #06
REPEATBEGIN:DJNZ R0, REPEATEXPRE
RET
REPEATEXPRE:MOV A, R0
DEC A
MOVC A, @A+DPTR
MOV P2, A
SJMP REPEATBEGIN
TAB: DB 00H,18H,3CH,7EH,0FFH
END
效果:
看红点儿就行了,二极管导通截止需要时间,所以显示是有延迟的:
……
五.判断语句的嵌套
对于判断语句的嵌套,除了多个if
叠加,还有一个就是switch
,这种方式比较局限,需要这几种方案的值是连续的,比如switch(i) i=0、1、2……
,因为单片机的代码是连续存放的,所以函数地址也可以由[基址]+偏移指针
的方式得到。
比如通过修改R0来修改P2的状态:
ORG 0000H
MOV R0, #0
LJMP MAIN
ORG 0100H
MAIN: LCALL SwitchInit
SJMP MAIN
SwitchInit: MOV DPTR, #Switch
CLR A
MOV A, R0
MOV B, #3
MUL AB
ADD A, #1
Switch: JMP @A+DPTR
LJMP FUN0
LJMP FUN1
LJMP FUN2
FUN0: MOV P2, #0H
RET
FUN1: MOV P2, #0FH
RET
FUN2: MOV P2, #0F0H
RET
如何理解?首先我们把偏移量×3了,是因为LJMP占用三个机器码:
所以我们只需要将偏移量A×3+1,就可以得到我们跳转语句所在的地址。
该代码等同于:
void SwitchInit(int A)
{
switch(3*A+1)
{
case &FUN0: FUN0();
case &FUN1: FUN1();
case &FUN2: FUN2();
}
}
// SwitchInit并不会反回到CALL时入栈的地址,而是在FUN里通过RET,将PC指向入栈前地址(听不懂这句话也没关系)。
六.循环语句的嵌套
对于刚才写的一重循环
R2 = 5; // REPEATINIT
while(--R2 > 0) // REPEATBEGIN
{
++A; // REPEATEXPRE
}
return; // RET
我们可以给它生个级:
R1 = 5; // REINIT
while(--R1 > 0) // RE1BEG
{
R2 = 5; // express
while(--R2 > 0) // RE2BEG
{
// express
}
}
return; // RET
比如,写个流水灯:
ORG 0000H
LJMP MAIN
ORG 0100H
MAIN: MOV A, #01H
LOOP: MOV P2, A
RL A
LCALL REINIT
SJMP LOOP
REINIT: MOV R1,#20
RE1BEG: DJNZ R1,RE1EXP
RET ;RE1END
RE1EXP: MOV R2, #200
;More express
RE2BEG: DJNZ R2, RE2EXP
SJMP RE1BEG ;RE2END
RE2EXP: NOP
SJMP RE2BEG
当然,若没有RE2EXP的话,可以直接原地跳:
REINIT: MOV R1,#20
RE1BEG: DJNZ R1,RE1EXP
RET ;RE1END
RE1EXP: MOV R2, #200
;More_express
RE2BEG: DJNZ R2, $
SJMP RE1BEG ;RE2END
嗯,看起来很啰嗦。
七.要背的一些寄存器和溢出值算法