(一) 研究概述
数据不仅可以存储在寄存器中,还可以存储在内存中。这次我们就研究在C语言中,怎样直接在内存中存储数据。以及这样做的一些延伸问题。另外,在附录研究中,我们还探究了C语言中循环和分支结构的实现。
(二) 研究过程
1) 直接在C语言中使用内存空间
此处援引书中的话:
对于存储空间来说,要使用他们一般都需要给出两个信息:一是指明存储空间所在、是哪个的信息;二是指明存储空间有多大的类型信息。
对于寄存器来说,就需要给出寄存器的名称,寄存器的名称就也包含了他们的类型信息。
对于内存空间来说,就需要给出地址(准确的说,是内存空间首地址)和空间存储数据的类型。
我们知道,在C语言里,用指针型数据来表示内存空间的地址和空间存储数据的类型。比如要向偏移地址为2000h、存储一个字节的内存空间写入一个字符‘a’,我们用如下的方法:
*(char *)0x2000 = ’a’;
第一个‘*’表示要访问的是一个内存空间;
“0x2000”是一个数值(0x表示十六进制),“(char *)”里面的‘*’指明了这个
数值表示一个内存空间地址,“char”指明了这个地址是存储char型数据的内存空间地址。
当然也可以用给出段地址和偏移地址的方法访问内存空间,比如我们要向地址为2000:0、存储一个字节的内存空间写入字符‘a’,如下:
*(char far *)0x20000000=’a’;
“far”指明内存空间的地址是段地址和偏移地址,“0x20000000”中的“0x2000”给出了段地址,“0000”给出了偏移地址。
由此,我们知道了C语言直接访问内存空间的基本方法。
2) 编写一个直接访问内存的C语言程序
我们编写一个程序um1.c如下:
编译链接完成,debug加载反编译如下:
3) 编写一个程序,用一条C语言实现在屏幕中间显示一个绿色的字符“a”
我们编写源程序如下:
编译链接运行,效果如下:
4) 关于全局变量和局部变量
我们分析下面程序中所有函数的汇编代码:
编译链接之后debug加载,反汇编如下。
在这里,我们非常直观的看出。程序中的全局变量,被放在了程序的数据段中,而局部变量则放在了其栈段中。这也正好说明了在每个函数开头,都有push bp;mov bp sp的作用。我们把局部变量放在了栈段中,那我们使用时必然要顺利的找到这个变量。而使用sp的话,假如程序中有入栈出栈的操作,栈顶指针变了,我们就找不我们存放的变量了。所以使用BP这个寄存器,在程序的开始把Sp的值给BP,然后,将SP的值增加,把局部变量的位置留出来。这时,我们可以方便的用BP找到局部变量,而也可以方便的使用栈的功能。函数返回时,BP的值又赋值给SP,函数内的局部变量就消失了。这也就是为什么C语言中局部变量的值只在函数内有效的原因。
5) C语言函数返回值存放在哪儿?
我们研究了变量的存放位置,那么,C语言的函数返回值存放在什么地方呢?我们写如下的程序。
编译链接后反汇编分析:
我们看到020A处,为函数f()的汇编指令。前五句,根据前面的内容,我们很容易理解。但是不同的是,多了一句mov ax,[01AA]。那么这条语句的作用是什么?会不会跟我们函数返回值有关?是不是函数返回值是用AX传出的呢?
我们调试运行程序:g 021d,结果如下
可见,函数的返回值的确是由寄存器ax传出的。
6) 综合分析
我们编写一个程序如下:
首先我们要了解#define和malloc这两条语句。
#define的作用,是宏定义,简单的讲在这里就是将((char *)*(int far *)0x02000000)这条语句用Buffer代替,以后再用到这条语句的时候,直接用Buffer就可以了。
((char *)*(int far *)0x02000000)表面是一个char型的指针,它指向的是0200:0000这个地址。
malloc(X)的作用,是申请X长度的内存。
Buffer=(char *)malloc(20)是申请20个字节的内存单元,将首地址赋值给Buffer指向的内存单元。
理解了这个,我们可以理解这个C程序的内容
我们编译链接,反汇编如下:
我们单步执行中,发现
AX中存放的应该是由malloc申请返回来的。应该是我们申请内存的首地址。
我们继续执行
我们发现这个地址果然是malloc返回来的申请内存的偏移地址。
(三) 附录研究
注:附录研究与内存单元的使用无关,主要是研究C语言中的三种结构。顺序结构我们可以很容易的理解,循环结构和选择分支结构研究如下:
1. C语言中循环结构for语句的实现
C语言中循环语句,for语句是如何实现的呢?我们编写如下程序
我们尽量的减小问题的复杂程度,使其突出我们所要研究的重点。这里循环内并没有其他的语句,仅仅是一条空语句。就是为了便于我们把问题的焦点放在for循环的实现上。
我们编译连接,加载后反汇编查看,如下:
我们可以看到,for的循环是使用了si寄存器,首先将si入栈保存,然后清空si,通过不断自加si的值,然后将si与我们原定的数值进行比较。
那么,如果在for中加入语句的话,那么这条语句会加在哪里呢?
我们分析,jmp 0203这条语句,这应该是转到循环内的。那么,应该是先比较,后执行循环内语句呢?还是先执行循环内语句,后比较呢?
我们接着分析,si的值是从0开始的,最后与5作比较,小于则转跳。如果先比较后执行的话,执行的次数就成了4次,这是不正确的。所以,应该是先执行,后比较。
我们验证:
我们在看jmp,这次直接jmp到了020B,果真是每次循环先执行,后比较。但是同时,我们发现,一进入for循环的时候,他是转跳到了比较语句。这又是为什么呢?
假如我们写这样的C语句:for(i=0;i<1;i++)会出现什么结果?如果不先比较一下的话,循环内语句必然要执行一次。这就是一进入for循环,必须先比较一下的原因。
2. C语言中分支结构if的实现
我们编写一个程序如下:
编译连接后加载查看如下:
我们可以直观的看到,依然是使用cmp,jnz等比较语句来进行程序的选择分支的实现。
所以,参照这个研究,返回头看我们的汇编程序。我们可以更加系统的使用汇编语言,来实现程序的各种选择,循环结构。
(四) 个人感悟
内存单元,寄存器,变量之间的关系到底如何?这次的研究给出了一部分答案。局部变量的内涵,全局变量的内涵,返回值的实现。拿研究C语言后的汇编知识在反观汇编程序的实现,应该会有更深入的理解,更结构化的编程概念。