汇编语言学习笔记.docx
《汇编语言学习笔记.docx》由会员分享,可在线阅读,更多相关《汇编语言学习笔记.docx(87页珍藏版)》请在冰豆网上搜索。
汇编语言学习笔记
汇编语言学习笔记
---傻瓜学汇编
前言
当我在学汇编的时候发现一到了实际编程就发现学过的那些指令串不起来,什么浮点数啊整数啊,怎么跳转啊,怎么循环啊,脑袋立马变成浆糊。
下面的文档是我的学习经历,
希望对初学者在学习加密解密,软件调试,单片机编程有点帮助。
1.编程环境的搭建
2.深入理解汇编语言的数据
3.顺序程序设计
4.分支结构程序设计
5.循环
6.数组及指针
7.函数
8.结构
9.综合运用
10.参考文献
一:
编程环境的搭建
首先装好masm32v10和windbg,和editplus,然后在editplus中输入下面的程序,具体的请参考罗云彬的那本书,里面有详尽的说明,编译运行看看:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
.data
msgdb"hello,thisisthefirsttestprogram!
",0dh,0ah,0
.code
start:
callmain
ret
mainproc
pushoffsetmsg
callprintf
addesp,4
pushoffsetmsg
callprintf
addesp,4
ret
mainendp
endstart
下面是运行结果:
这里输出两行消息主要是我在写这个最简单的程序的时候发现他不换行,于是我在数据定义后面我加了“0ah,0dh”,呵呵,就是回车换行的十六进制表示,你也可以用其他方法试试,
程序就不多解释了,后面会有更多的解释,不过你一定要走到这以步,才能进行下一章。
2.深入理解汇编语言的数据
整数常量及变量,先看一段很简单的汇编程序:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
.data
PRICEEQU30
msg1db"total=%d",0dh,0ah,0
.code
start:
callmain
ret
mainproc
localnum:
dword
localtotal:
dword
movnum,10
moveax,num
imuleax,eax,PRICE
movtotal,eax
pushoffsetmsg1
callprintf
addesp,4
ret
mainendp
endstart
程序的意思很简单就是在屏幕上打印出某个东西的价格,如过要你拿笔和纸算,拿你肯定很快就能算出来,但你让电脑怎么算呢?
当电脑执行到第一个语句的时候,也就是num=10,
它就把10放到某个地方并且记住这个值,寄存器或者内存,呵呵,它也就这两个地方,为什么要这么做呢?
因为后面要用它来计算啊,为了算出这个值,电脑好的办法就是放在它的内存里,为什么不是寄存器?
因为寄存器太少了,就那么几个,呵呵,所以了它就把10存在一个叫num的内存里,注意了哦,num是程序里的变量名,是存中里的一个位置的名称,它的值是10,你可能会问,不起名不行么?
行,等下在调试器中你看到的就是没名的。
来看看它在调试器中的样子:
num变成了[ebp-4]了,现在你想象有个几千行的程序如果都用[ebp-4]这样的名字的话,那我们会疯的,所以汇编程序就让我们给程序里面的变量起个直观的名字,而不是用具体的数字去让你去记住变量内存的位置。
程序中imuleax,eax,1eh中的1eh就是个整形常量,也就是30.现在你应该对常量和变量有点感觉了吧。
再看个例子:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
.data
adb12h
bdw1234h
c1dd12345678h
msg1db"thenumberis=%xh",0dh,0ah,0
.code
start:
callmain
ret
mainproc
moval,a
cbw
cwde
pusheax
pushoffsetmsg1
callprintf
addesp,8
movax,wordptra
cwde
pusheax
pushoffsetmsg1
callprintf
addesp,8
moveax,dwordptra
pusheax
pushoffsetmsg1
callprintf
addsp,8
ret
mainendp
endstart
首先,你得想a,b,c1三个变量在程序中到底是怎么存的,是12123412345678,还是78563412341212呢?
呵呵,用调试器载入程序看看就知道了:
哈哈,看到了没,正确的是这个:
00403000:
1234127856341274-68,这是为什么?
还有就是这个程序打印的三个结果又是什么呢?
是12h和0012h和00000012h吗?
如果是,那你就错了哦,应该是:
thenumberis=12hthenumberis=3412hthenumberis=78123412h
呵呵,首先,你得明白这三个你定义的数据在内存是怎么存的,一个原则就是你定义的数据的高位存在内存中的高字节地址,你定义的第二个数据:
1234h,高位字节是12吧,低位字节是34吧,所以编译器它先存34字节存在内存的低地址,然后再把12存在高地址,当然如果是你只定义了一个字节那顺序就没反了,就像你定义的第一个字节数据12好一样,同样第三个双自数据12345678h,编译器它就先存78好字节了,然后是56好字节,34h字节,12h字节。
下面我们来看看程序:
moval,a,就是是把12h放到al中,movzxax,al0扩展指令,将al中的字节扩展到ax中,不足的位用0填充,不改变al的值,al里面是什么值,扩展后ax的值还是等于al中的值。
movzxeax,ax;0扩展指令,将ax中的字节扩展到eax中,不足的位用0填充,不改变ax的值,al里面是什么值,扩展后eax的值还是等于ax中的值。
然后pusheax,和pushoffsetmsg1,callprintf就是调用c语言库函数printf打印消息,
就相当于c语言里面的:
printf("thenumberis=%xh\n",a);下面的和这段一样,我就不写废话了。
如果面对的是有符号数,那就得用movsx了,当然还有其他指令,后面再介绍。
浮点数:
在计算机内部,浮点数是以二进制表示的,所以,要先转换为二进制浮点数,转换分两部,整数部分的装换,采用“除2取余法”,小数部分的装换,采用“乘2取整法”,例如19.2,
先将19转换成二进制:
10011,然后将0.2转换成二进制:
00110011……0011,它是个无穷循环小数,然后就是规格化,分三种情况:
如果定义的数据类型是dword或者是real4,那么符号位占一位,阶码占8位,位数占23位,总共是32位,如果定义的类型是qword或real8,那么符号位占一位,阶码占11位,位数占52位,总共64位,如果定义的类型是real10或者是tword,那么阶码占15位,位数占64位,符号位占一位,总共80位。
怎么算阶码呢?
如果是32位,就将阶码加上127,然后转换成二进制,如果是64位,就加上1023,如果是80位,就加上16383。
我们看看怎么将19.2转换成32位的二进制浮点数:
首先将19转换成二进制:
10011,然后将0.2转换成二进制:
00110011……0011,整理成32位就是:
10011,001100110011001100110011001。
然后规格化为:
1,0011001100110011001100110011001x2的4次方,阶码为127加4等于131:
10000011。
所以当浮点数19.2表示为三种不同的数据类型为:
32位(dword,real4):
0,10000011,0011001100110011001101
64位(qword,real8):
0,100000000011,0011001100110011001100110011001100110011001100110011
80位:
0,100000000000011,001100110011001100110011001100110011001100110011001100110011001100110011。
转换成16进制就是4199999Ah,4033333333333333h,403999999999999999ah。
然后我们yong程序来验证一下对不对。
例子如下:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
.data
f1real419.2
f2real819.2
f3real1019.2
msg1db"thefloatingnumberis=%g",0dh,0ah,0
.code
start:
callmain
ret
mainproc
localf:
real8
flddwordptrf1
fstpf
pushdwordptrf+4
pushdwordptrf+8
pushoffsetmsg1
callprintf
addesp,12
pushdwordptrf2+4
pushdwordptrf2
pushdwordptroffsetmsg1
callprintf
addesp,12
fldf3
fstpqwordptrf
pushdwordptrf+4
pushdwordptrf+8
pushoffsetmsg1
callprintf
addesp,12
ret
mainendp
endstart
程序很简单,就是分别在屏幕上打印三个浮点值,如下图:
在这里我要说明下,我只有把32位和80位的转换为64位的,才能打印成功,这可能是库函数printf的原因,怎么转换呢?
32位浮点转换64位浮点:
首先得借助一个64位的浮点局部变量:
localf:
real8
flddwordptrf1
fstpf
第一句定义了f位一个64位的浮点局部变量,第二句就是把32位浮点数转换为80位的,然后第三句就是把80位的转换位64位的。
80位浮点转换位64位浮点数:
同样借助一个64位的浮点局部变量:
fldf3
fstpqwordptrf
第二句就是把80位的浮点转换位60位的。
但是这两句怎么解释呢:
pushdwordptrf2+4
pushdwordptrf2
为什么要先把f2的高4位字节入栈呢?
好,我们先来看看这个数转换成64位的16进制为:
4033333333333333h,前面我说了高低对应原则,那么这个64位的16进制在内存中高4字节地址应该存40333333h,也就是存它的高4字节,然后是33333333h,但是,呵呵,在堆栈中的地址是从高往低增长的,所以我们应该先把这个数的高四字节入栈,也就是40333333h,怎么在内存中得到这高4字节呢?
就是从f2+4处压入4字节就可以了,然后就是低4字节入栈。
如果还没理解,用cdb调试一下就清楚了。
浮点与整数之间的转换:
先看例子成ch2-4:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
.data
f1real819.2
f2dword20
msg1db"thefloatingtointnumberis=%d",0dh,0ah,0
msg2db"theinttofloatingnumberis=%f",0dh,0ah,0
.code
start:
callmain
ret
mainproc
localf:
real8
fldf1
fistpdwordptrf
pushdwordptrf
pushoffsetmsg1
callprintf
addesp,8
fildf2
fstpf
pushdwordptrf+4
pushdwordptrf+8
pushoffsetmsg2
callprintf
addesp,12
ret
mainendp
endstart
运行结果为:
浮点数转换成整数:
fldf1
fistpwordptrf
首先我们还是借助了一个64位的局部变量,先把浮点数装入浮点寄存器,然后用装换整行的指令变成整数再存入一个局部变量就行了。
整数转换成浮点数:
fildf2
fstpf
先把整数用装换指令装入浮点寄存器,然后把浮点数存到一个局部变量就可以了。
我在后面会详细说名浮点数的运算和浮点寄存器的。
字符与字符串常量:
怎么定义他们?
他们是以什么形式存在计算机中?
首先我们怎么在汇编中定义他们呢:
先看看例子ch2-5:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
.data
str1db'thisisastringtest',0ah,0dh,0
str2db"thisisastringtest",0ah,0dh,0
.code
start:
callmain
ret
mainproc
moveax,offsetstr1
pushoffsetstr2
callprintf
addesp,4
pushoffsetstr1
callprintf
addesp,4
ret
mainendp
endstart
程序就是在屏幕上打印两行消息,下面是运行结果:
然后我们用cdb调试器看看定义的那两个字符串变量在内存中到底是怎么样的:
恩,它们是以asii码的形式存在的。
其他的数据类型我会在下面的各个章节会随着编程的算法和调试一起讲解。
3:
顺序程序设计
汇编语言的顺序编程比较好理解,就是在编程的时候没有跳转,没有循环,看看例子ch3-1:
例ch3-1:
输入三角形的边长,求三角形的面积。
我假设输入的三边长都是能构成三角形的,求三角形面积的公式为area=√s(s-a)(s-b(s-c)。
s=(a+b+c)/2.
这里要用到浮点指令,那就先回顾下浮点指令的用法:
这里要加减乘除和平方根五种指令,由于Intel的浮点数据寄存器是种堆栈结构,我们要记住这一点。
先看看数据传送指令:
fld和fild,fst,fstp:
fld源操作数,源操作数可以是浮点寄存器和内存,这个指令主要是把源操作数压入浮点寄存器堆栈(其实就是st0),如果源操作数是整数,那就用fild。
Fst和fstp是把st(0)浮点寄存器中的数弹出到目的操作数中,目的操作数可以为浮点寄存器和内存。
加减法指令:
fadd,faddp,fub,fsubp
第一种形式:
fadd目的操作数,源操作数。
其中目的操作数,源操作数可以为浮点寄存器和内存。
第二种形式:
fadd源操作数,我本人比较喜欢这种,它不会把我脑海里的浮点寄存器的顺序弄乱,这种形式的源操作数只能是内存。
减法指令同加法指令,就不多说了。
乘除法指令:
fmul,fdiv
浮点的乘除法是不区分有符号和无符号数的,他们也有两种形式:
fmul目的操作数,源操作数和fmul源操作数这两种形式,第一种操作数和源操作数可以为浮点寄存器和内存,但第二种源操作数值能为内存。
平方根指令:
fsqrt
这个指令就一种形式就是fsqrt,就是把第0个浮点寄存器st0的值变成平方根值然后存在st0中。
再回到例题中,我们应该先算s值,然后再算平方根下面的值然后求平方根就行了。
S值是三边长除以2,转换成浮点指令就是:
flda;先把a值放到浮点寄存器st0中
再就是faddb;这个就是a+b,结果存在st0中
然后faddc;同上,结果存在st0中
现在算出了三边长的和,在除以2就ok了fivtwo;结果在st0中,
最后把st0里面的值用fstps存到s中就把s值算出来了。
再来算根号下面的值,这里有乘法和减法,我们先算减法:
flds
fsub,a;这个就是s-a,结果在st0里
再flds
fsubb;这个结果还是在st0里,但是上面那个s-a已经被推到st
(1)这个浮点堆栈了啊,记住了。
再flds
fsubc;这个结果还是在st0里,
那么s-a被推到st
(2)了,s-b被推到st
(1)了,s-c的值在st(0)中,这里千万不能及乱了啊,下一步算乘法,先算s*(s-c),muls;果在st0li
fmulst(0),st
(1)这一步是算s*(s-a)*(s-b)
最后算s*(s-a)*(s-b)*(s-a)fmulst(0),st
(2),
然后把s*(s-a)*(s-b)*(s-a)的结果再求平方根fsqrt
再把这个平方根的值弹出到area中。
这是按照上面的想法写出的程序:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
scanfprotoC:
VARARG
.data
msg1db"%f,%f,%f",0
msg2db"area=%7.2f",0ah,0dh,0
msg3db"pleaseinputthreefloatingnumbers",0ah,0dh,0
tworeal42.0
.code
start:
callmain
ret
mainproc
locala:
real4
localb:
real4
localc1:
real4
locals:
real4
localarea:
real8
pushoffsetmsg3
callprintf
addesp,4
leaeax,c1
pusheax
leaeax,b
pusheax
leaeax,a
pusheax
pushoffsetmsg1
callscanf
addesp,16
flda
faddb
faddc1
fdivtwo
fstps
fsuba
fsubb
fsubc1
fmuls
fmulst,st
(1)
fmulst,st
(2)
fsqrt
fstparea
pushdwordptrarea+8
pushdwordptrarea+4
pushoffsetmsg2
callprintf
addesp,12
ret
mainendp
endstart
编译运行:
下面是结果:
但是出乎意料哦,错了,怎么办呢?
你发现什么问题了没?
那只有调试看看了,用
cdb-2C:
\w\b\ch3\ch3-1\ch3-1打开程序然后逐步调试看看,当跟踪到下面这句是就发现有问题了:
就是fstp,s,也就是00401041d95df0fstpdwordptr[ebp-10h]ss:
0023:
0013ffac=a9e45d08,然后发现st中的值被弹走了,但紧接着又用st(0)用与下一步进行计算了,先推出调试,改动下再运行看看对不对,但结果还是错的:
那还得继续调试了,发现我对fsub指令理解错了,原来这个指令执行完之后它不把结果推到下一个浮点堆栈:
那只能用fsubp了,改动如下:
flda
fsubpst
(1),st
flds
fldb
fsubpst
(1),st
flds
fldc1
fsubpst
(1),st;然后改成这种了
但结果还是错的,一看到这句pushdwordptrarea+8
pushdwordptrarea+4
想一下“高低原则”,改成pushdwordptrarea+4
pushdwordptrarea+8
编译运行,哈哈,ok了:
最后正确的代码:
.386
.modelflat,stdcall
optioncasemap:
none
includelibmsvcrt.lib
printfprotoC:
VARARG
scanfprotoC:
VARARG
.data
msg1db"%f,%f,%f",0
msg2db"area=%7.2f",0ah,0dh,0
msg3db"pleaseinputthreefloatingnumbers",0ah,0dh,0
tworeal42.0
.code
start:
callmain
ret
mainproc
locala:
real4
localb:
real4
localc1:
real4
locals:
real4
localarea:
real8;这里是变量定义
pushoffsetmsg3
callprintf
addesp,4;这里是提示输入三个边长
leaeax,c1
pusheax
leaeax,b
pusheax
leaeax,a
pusheax
pushoffsetmsg1
callscanf
addesp,16;这里是调用库函数scanf
flda
faddb
faddc1
fdivtwo