一、 学习过程
在高级语言中我们为什么要用变量呢?因为我们要存储数据,而且因为要使用循环等语法结构,存储的数据需要不断地变化,变量的特性可以很好地解决这个问题。在前面我已经讨论过了,变量的声明实际上就是在内存中开辟一个内存空间,我们在汇编语言里使用循环,主要是把数据存在si、di等寄存器中来进行操作,存储数据是把数据放在寄存器、内存空间(普通的和栈)里面。编写程序ur1.c,并编译连接:
用debug加载ur1.exe,用u命令查看编译后的机器码和汇编代码:
发现main函数中的代码没有出现,用t命令单步执行发现程序在一个死循环内,为什么这时显示的代码不是main函数里的代码呢?可能的原因有两个:(1)我以为main函数里的代码编译后会是这样:mov ax,1;mov bx,1;mov cx,1;mov ax,bx;add ax,cx……但是c语言编译成的汇编代码是通过别的方式实现代码的功能的,与我从汇编的角度来实现功能的方式不一样。(2)main函数本身经过编译后会有许多汇编代码,或者是main函数前面还要加载其他程序,导致main函数里的代码被放在一个我们不知道的地方,所以找不到。那么main函数的代码在什么段中?用debug怎样找到ur1.exe中main函数的代码?我觉得应该在code段中,但是程序里没有标号,看不到main的地址。那么就写一个main到程序里,看看main的地址是多少。
编译程序,显示main函数的偏移地址:
运行发现,main函数的偏移地址为1fa:
那么,用debug运行ur1.exe,找到main函数的地址:
由图可知,ur1.c中对应的代码是mov ax,1;mov bx,1;mov cx,2;mov ax,bx;
add ax,cx;mov ah,bl;add ah,cl;mov al,bh;add al,ch.这里还应注意到开头和结尾的三条指令:push bp;mov bp,sp;pop bp.是对bp进行了保护,并且在程序中把sp赋给了bp,为什么要这样做呢?书本上的解释是:这是C编译器安排的为函数中可能使用到bp寄存器而设置的。那么为什么函数中可能使用到bp寄存器呢,也可能使用到别的寄存器啊,为什么只对bp寄存器进行了保护?
还有在代码结尾处用ret进行了处理,即对ip出栈,一般用到ret时会与call指令配合使程序段成为可调用的字程序段,那么main函数是以call-ret的方式来实现成为子程序段的吗?将以下程序编译连接:
用debug查看main函数中的内容:
发现果然f()的位置被call 020b代替。而20b的位置上是f()函数的内容:
所以我们的想法是正确的,不仅main函数在汇编语言里是以call-ret程序段的方式实现的,甚至C语言是将所有函数都实现为汇编语言里的call-ret子程序段。
二、 解决的问题
(1) 为什么用debug查看ur1.exe文件找不到main函数里的内容?
答:因为main函数及其内容是作为子程序段被放在其他位置,所以找不到。
(2) 用debug怎么查找函数的偏移地址?
答:写一个程序,用printf打印函数的地址。
(3) C语言里的函数在汇编语言里是怎样实现的?
答:用call-ret作为子程序段来实现。
三、 未解决的问题
(1) 为什么一个函数在汇编语言中实现要对并且只对bp寄存器进行保护?
(2) 为什么用debug加载ur1.exe用u命令首先看到的不是main函数及其内容,而是一大堆无关的指令?
(3) 其他高级语言函数的实现方式和C语言一样吗?
四、学习感想
你的想法到底正不正确呢?试一试就知道了,实践是检验真理的唯一标准。很多时候我们都会觉得尝试是一件很麻烦的事,所以面对还不太懂的问题,就想当然地给它下一个判断,或者根本不去想,等别人来告诉你,久而久之,我们对一件事物的整个认知就会出现偏差,从而犯错。函数名能够直接写在printf函数里把地址打印出来吗?c语言里的函数是用什么形式在汇编语言里实现的呢?这些都是需要亲自尝试才能解决的问题。在平时的学习和生活中,我们经常会遇到诸如此类的问题,但是有时候因为自身的惰性,想当然地把它忽视掉了,要用到的时候才感叹自己知道的真的太少。所以,保持一个积极强烈的求知心态是学习过程中很重要的。