TURBOC六种编译模式概述
by other
2011-05-29 09:49:58
六种编译模式概述
Turbo C 提供了六种编译模式。编译模式有时也称为寻址模式或
内存模式,因为它处理的就是如何在内存中为程序,数据,堆栈分配
空间并存取它们,这六种模式是:微模式tiny,小模式small, 紧凑
模式compact,中模式medium,大模式large,巨模式huge。它们之间
的关系如下表所示。 │ 小程序 │ 大程序
━━━━┿━━━━━━┿━━━━━━━━
小数据 │ 微,小 │ 中
大数据 │ 紧凑 │ 大,巨 所谓小程序就是只有一个程序段,当然不超过64K 字节,缺省的
码(函数)指针是near。所谓大程序就是有多个程序段,每个程序段不
超过64K字节,但总程序量可超过64K字节,缺省的码指针是 far。所
谓小数据就是只有一个数据段,缺省的数据指针是near。所谓大数据
就是有多个数据段,缺省的数据指针是 far。下面还会逐个谈到它们
之间的差别,并通过同一程序在六种不同模式下的输出结果,来进一
步加深对这六种模式的理解。但先要强调一点:无论使用哪一种编译
模式,单个的Turbo C源文件不可能生成大于64K字节的代码,也不能
生成大于64K字节的静态(包括全局)数据。例如下面这个程序: int a[15000],b[20000];
void main(){}
在任何模式下都不能编译。这是因为,两个数组所要求的总存储量达
70K字节。编译时会报出"Too much global data defined in file"
的出错信息。为了处理大于64K 字节的代码或静态数据,必须分成几
个源文件。以上面这个程序为例,可以分成文件A1.C和A2.C,分别用
巨模式对这两个源文件进行编译,最后连接成一个可执行文件。 al.c a2.c a.prj
int a[15000]; int b[20000]; a1
void main() a2
a1.obj(30k) a2.obj(40k) a.exe(71k)
六种编译模式的差别是:它们对来自不同源文件的码和数据段的
处理不同,对动态分配的堆空间的处理不一样,对指针使用也不一样。
此外,它们的所形成的 .obj 文件中传给连接程序的信息也不一样,
以便连接程序相应地安排码段和数据段,把相应的说明放在 .exe 文
件的头中并借此通知DOS:当执行这个程序时如何装入码段和数据段,
如何设置各个段寄存器。
用于演示六种编译模的程序是由两个源文件X.C和Y.C组成,如下
所示:
/* X.C */
#include<general.h>
void a()
{
static int b;
int c;
printf("In function A \n");
printf(" CS : %X \n",_CS);
printf(" DS : %X \n",_DS);
printf(" SS : %X \n",_SS);
printf(" Static B : %p \n",&b);
pritnf(" Automatic C : %p \n",&c);
}
/* Y.C */
#include<general.h>
int d;
void main(){
int e;
a();
printf("In function main \n");
printf(" CS : %X \n",_CS);
pritnf(" DS : %X \n",_DS);
pritnf(" SS : %X \n",_SS);
pritnf(" Global D : %p \n",&d);
pritnf(" Automatic E : %p \n",&e);
printf(" Heap address : %p \n",malloc(2));
#if defined(__TINY__)||defined(__SMALL__)||defined(__COMPACT__)
pritnf("Function A : %Np \n",a);
pritnf("Function main : %Np \n",main);
}
#else
printf("Function A : %Fp \n",a);
printf("Function main : %Fp \n",main);
}
#endif 第一个源文件包含函数a和一个静态(局部)变量b,第二个源文件
包含主函数main和一个全局变量d 。两个源文件中各含有一个自动变
量c和e。第二个源文件的主函数main调用了第一个源文件中的函数a,
还调用了Turbo C 的库函数malloc去分配一块堆空间。两个源文件是
分别编译的,然后再通过连接程序连接起来。
通过以六种不同模式编译这两个源文件,可以看到它们是如何为
码、数据和堆栈段分配空间,可以看到静态变量、自动变量和堆变量
分别存放在什么位置,函数放在什么地方。正如下面将要看到的那样,
在某些模式下,数据指针是near而函数指针是 far;在另一些模式下
情况又正好相反。
对于数据指针,不管是far还是near,pritnf函数中的格式说明 %p都
能把指针正确地打印出来。对于函数,指针%p就没有这个功能。所以,
在main函数中必须加条件编译控制行#if、#else和#endif。微模式
在微模式下,整修程序只有一段,这个段内包含码、静态和全局
数据、堆栈和堆。因为只有一个段,在执行时DOS将把寄存器CS、DS、
SS设置为相等,全都指向这个段。在这个段内,码是首先装入的,地
址最低,接着是静态变和全局变量,然后是堆,最后地堆栈。堆和堆
栈都是动态的,堆从低地址往高地址增长,堆栈从高地址往低地址增
长。若两者相碰,则表示内存空间已耗尽。在微模式下,所有指针都
是near,且都是相对于寄存器CS、DS和SS的。对于用微模式编译并连
接生成的 .exe文件,DOS的exe2bin实用程序转换为 .COM文件。
从下表演示程序的输出结果可以看出,函数a 比函数main的地址
低,变量b比变量d的地址低。这是因为,在连接时是x.obj在前,
y.obj在后。
微模式 小模式 紧凑模式 中模式 大模式 巨模式
In function A
CS : 74C8 74B1 74B1 74F9 74FD 74FE
DS : 74C8 75CC 7629 75EC 764A 7674
SS : 74C8 75CC 767A 75EC 76A0 76BB
Static B : 1704 048C 7629:04C8 049A 764A:04D6 7674:0002
Automatic C : FFD0 FFD0 767A:0FD6 FFCC 76A0:0FD4 76BB:0FD0
In function main
CS : 74C8 74B1 74B1 74FE 7502 7503
DS : 74C8 75CC 7629 75EC 764A 767B
SS : 74C8 75CC 767A 75EC 76A0 76BB
Global D : 1706 048E 7629:04CA 049C 764A:04D8 767B:0004
Automatic E : FFD6 FFD6 767A:0FDE FFD4 76A0:0FDC 76BB:0FDA
Heap Address: 1792 051A 777C:000C 0568 77A2:000C 77BD:000C
Function A : 0283 01A5 0167 74F9:000E 74FD:000D 74FE:0003
Function main: 02C1 01E3 01AE 74FE:0004 7502:000C 7503:0009小模式
小模式是常用的模式,本书中大部分例子都是用小模式编译的。
虽然小模式与微模式一样,都是小数据、小程序模式,但它与微模式
有两点重要的差别。第一,码和数据/堆栈/堆段是分开的,所以CS不
等于DS和 SS。第二,除了和数据/堆栈共用一个段的堆外,还有一个
远堆,以far指针进行存取。从数据/堆栈段的末尾直到常规内存的末
尾都是属于远堆。
因为码、静态数据和(近)堆仍然在同一个段内,所以小模式下缺
省的数据指针和函数指针都是near。结果,在小模式下不能直接通过
该模式下的Turbo C 函数来处理远堆中的变量。然而,只要程序提供
自己的操作函数,就可以存取整个远堆中的任一单元,即可以使用整
个常规内存。中模式
在数据/堆栈/堆的分配方面,中模式与小模式是一样的,差别在
于码段的分配。在中模式下,来自不同源文件的码模式放在不同的码
段内。严格地讲,同一源文件内的各函数也是放在不同码段内。各码
段的总空间数只受微机上可用内存的限制。因为有多个码段,所以
Turbo C必须用far函数指针。在演示程序输出的结果中函数a 的地址
为74F9:000E,函数main的地址为74FE:0004。函数a 的地址较低,是
因为在连接时包含函数a的x.obj在前。在中模式下,堆仍然有近堆和
远堆之分。紧凑模式
紧凑模式在概念上是最简单的,码、静态数据、堆栈、堆各有其
自己的段。堆只有远堆,没有近堆。像小模式和中模式中的远堆一样,
堆是用far指针来存取的。可以用Turbo C的库函数来处理堆变量。所
有数据指针都是far,函数指针都是near。 从演示程序的输出中可以
看出,CS、DS、SS三个寄存器的值彼此不等。值得注意的是,静态数
据的总量仍不可超过64K字节。大模式
在静态数据/堆栈/堆的分配方面,大模式等同于紧凑模式。在码
的分配方面,大模式等同于中模式。无论是数据指针还是函数指针,
一律都是远指针。与紧凑模式一样,静态数据的总量不可超过64K 字
节。巨模式
巨模式取消了静态数据总量不可超过64K 字节的限制。来自不同
源文件的码放在不同的段内,来自不同源文件的静态数据也是放在不
同的段内,只有堆栈是合在一起的。以前举的例子就是利用了这一特
点。从演示程序的输出中也可以看,当从函数main内调用函数a 时,
不但CS改变了,DS也改变了。当然,两个函数共用了同一个堆栈,否
则就无法正确返回。应该注意的是,不要把巨模式和巨指针混为一谈,
在巨模式下缺省的指针仍是far而不是huge。堆栈的组织
Turbo C 堆栈是用来存储其生命期与函数生命期相同的数据,这
样的数据包括函数参数和函数体内定义的自动变量。为了表明函数堆
栈内部各数据的存放关系,设有这样一个函数定义: long f(char a,int b)
{
int c;
char d;
....
每当调用函数f 时,调用进行首先按相反顺序,即按从右到左的
顺序把调用参数压入堆栈。本例就是先压入 b,再压入a。尽管参数a
是字符型,但仍压入16位,因为80X86的机器没有8位的压栈指令。在
压入参数之后,根据调用指令是near还是far,再压入 2个或4个字节
的返回地址。
进入被调用函数 f之后,它首先把寄存器BP的当前值压入堆栈,
并把SP寄存器的值拷贝到BP寄存器。接着再次执照相反的顺序在堆栈
内建立起函数体内的各个自动变量,本例里就是先d后c。直到此时,
堆栈的内容将会如下所示:
....
b
a
返回地址
保留的BP
d
c 这里之所以要对BP和SP 作如此处理,目的有3个。第一,为了利
用BP作地址寄存器,通过[BP±n] 这样的寻址方式到堆栈中存取调用
者传过来的参数和被调用函数自己的自动变量。因为在80X86中规定,
当用BP作地址寄存器时,缺省的段地址是SS而不是DS。第二,腾出DS
和其它地址寄存器,仍用来存取缺省的数据段内的数据。第三,腾出
SP以便在函数体内再调用其它函数。
当函数 f完成了它的工作以后,它就把返回值放到相应的位置。
如果返回值是char型,则在返回前先强制转换为 int型。凡是返回值
占两个字节的都通过寄存器AX返回,凡是返回值占 4个字节的都是通
过寄存器对DX:AX返回。超过4个字节的struct返回值,则被放在一个
静态变量内,返回的是指向这个变量的指针。dboule返回值是放在数
值协处理器的top_of_stack寄存器或协处理器软件模拟包内与这个寄
存器等效的地方。接着函数f 把BP拷贝到SP,从堆栈中弹出入口时保
留的BP值到BP寄存器。最后执行一条near 或far返回指令,返回到调
用者。返回以后,调用函数必须把调用调用时压入堆栈的参数从堆栈
中清除。
上面这一套函数调用规则就叫做 C调用规则。从这个过程中可以
看出,调用函数和被调用函数在参数数目上可以不一致。如果调用函
数压了过多的参数,被调用函数不存取这些多余参数是没有什么影响
的,调用函数在重新获得控制权后,会正确地把这些参数清除掉。如
果调用函数压入了过少的参数,被调用函数就可能把一些并非参数的
内容取来人微言轻参数而产生意想不到的结果。为了克服这个困难,
如果参数数目是不定的,那么第一个参数最好是说明随后的参数的个
数。
另一套不同的函数调用规则叫做PASCAL规则,它与 C调用规则有
两点重要的差别。第一,压入参数的顺序是从左到右。第二,被调用
函数的工作完成以后,从堆栈中弹出参数是由被调用函数而不是由调
用函数去完成。PASCAL调用规则要求调用函数和被调用函数参数上数
完全一致。顺便说一句,Turbo PASCAL语言使用的不是PASCAL调用规
则,而是一种更为精心设计的堆栈格式,使得从被嵌套的函数内可以
存取函数的自动变量。堆的组织
前面已经说过,在小模式和中模式下,堆有近堆和远堆之分,处
理办法也不一样。近堆和堆栈共享一个段,它们相向增长,如果相遇,
则说明缺省数据段已耗尽。远堆使用了缺省数据段之上直至常规内存
末尾的整个空间。为了管理这两个堆,Turbo C 提供了两组相应的函
数:
coreleft farcoreleft
realloc farrealloc
malloc farmalloc
free farfree
calloc farcalloc 左边的近堆函数用近指针寻址各个堆变量,所用的参数也都16位
的unsigned型。右边的远堆函数用远指针寻址各个堆变量,所用的参
数也都是unsigned long型。
在微模式下没有远堆,在紧凑模式、大模式和巨模式下只有一个
不改堆,其组织形式如同远堆。但在这三种模式下,既可以使用近堆
函数也可以使用远堆函数存取堆中变量。这是因为,不管使用哪一种
堆函数,这三种模式决定了所有数据指针是far。如果使用近堆函数,
则表示所需容量的参数size还必须是unsigned型的16位数。如果必须
处理大于64K字节的内存块,还必须使用远堆函数。
分配和释放是随机进行的,没有一定的次序,结果就造成了各个
堆变量在堆中是不连续的。Turbo C 用一个链表来处理这些堆变量。
在每一个堆变一的前面都有一个头,头中包含两个信息:此变量的长
度和指向下一个堆变量的指针。对于小数据模式,每个头占4个字节,
对于大数据模式,每个头占8个字节。
为了说明分配、释放、再分配在堆中是如何进行的,请看下面这
个演示程序htap.dem的输出结果。#include<stdio.h>#define report printf("coreleft=%u\n",coreleft());void main()
{
void *p,*q,*r;
printf(" ");report;p=malloc(1);
printf("p=malloc(1) =%p;",p);report;q=malloc(2);
printf("q=malloc(2) =%p;",q);report;q=realloc(p,3);
printf("p=realloc(P,3)=%p;",p);report;r=malloc(1);
printf("r=malloc(1) =%p;",r);report;free(q);
printf(" free(q) ");report;free(p);
printf(" free(p) ");report;
}
这个程序产生的输出如下:
coreleft = 63952
p=malloc(1) = 0500; coreleft = 63946
q=malloc(2) = 0506; coreleft = 63940
p=realloc(P,3) = 050c; coreleft = 63932
r=malloc(1) = 0500; coreleft = 63932
free(q) coreleft = 63932
free(p) coreleft = 63946 这个演示程序是用小模式编译的。首先,coreleft报告可用的内
存量。其次,malloc建立单字节堆变量p和双字节变量q。因为总是以
2字节整数倍进行分配的,所以单字节变量p实际上也占用两个字节的
空间。每个堆变量还需要 4个字节的头。这样,每分配一个堆变量,
内存容量就减少6个字节。接着,realloc把变量p扩大到3个字节,这
就要求重新分配,返回的指针也指向了新地址050C。重新分配的堆变
量p占用了8个字节。包括它的头。尽管此时原来占用的 6具字节已经
释放了,但coreleft仍报告减少了 8个字节,而不是减少了两个字节。
这是因为coreleft报告的只是堆中最上面最后一个变量之后连续可用
的内存容量。也就是说由于堆的碎片化,coreleft报告的值是不准确
的。接着,程序又分配了一个单字节变量 r,它占用了第一次分配给
变量p后来又被释放的那6字节空间。在些之后,程序释放变量 q,在
变量r和p之间留下一个空洞。应该注意,分配r和释放q都不影响
coreleft 报告的值。最后,程序释放变量p。此时,coreleft报告的
值才是准确的,因为堆中只在其开始部分剩一个变量r了。
下面这个farheap。dem程序演示了如何从远堆中分配一个大于
64K字节的数组a。数组a是由9000具double型元素组成的,共需72K
字节。把函数farcalloc返回的远指针强制转换为huge指针, 以后就
可以用这个huge指针存取数组中的各个元素。#include<malloc.h>
void main(){
int i,n=9000;
double huge *a;
double sum;
a=(double huge *)farcalloc(n,sizeof(double));
for(i=0;i<n;a[i++]=i);
for(i=0,sum=0;i<n;sum+=a[i++]);
printf("a[i]=i for i=0。 n-1; n=%d\n",n);
printf("sum of all a[i]=%8.0f\n",sum);
printf("(n-1)n/2 =%1d \n",(long)n*(n-1)/2);
}
其它内存操作函数
Turbo C 中还提供了许多有关内存拷贝、比较、设置和查找的函
数。这些函数的说明都在头文件mem.h 中。一般来说,它们都不牵涉
到什么结构,而是直接对内存进行操作。这些函数可对简单的字节进
行操作,也可实现C 语言不直接支持的对数据结构的操作,如用一个
数组对另一个数组赋值,数组或C结构之间的比较等。
用于内存之间拷贝数据的Turbo C函数有如下5个:
void *_Cdecl memccpy(void *dest,const void *src,
int c,size_t n);
void *_Cdecl memcpy(void *dest,const void *src,
size_t n);
void *_Cdecl memmove(void *dest,const void *src,
size_t n);
void _Cdecl movmem(void *src,void *dest,unsigned length);
void _Cdecl movedata(unsigned srcseg,unsigned srcoff,
unsigned dstseg,unsigned dstoff,size_t n); 函数memcpy从源 src拷贝kn个字节到目dest。如果源和目有重叠
的地方,则结果不一定正确。
函数memccpy与memcpy类似,但若被拷贝的字节中有字符c,则在
拷贝完这个字符后也停止拷贝,返回的指针指向目dest中的下一个字
节位置。若n个字节全部拷贝完,则返回的指针为空。
函数memmove和movmem 也用于拷贝,但它们都解决了源和目重叠
的问题。函数movmem 一反通常“目=源”这样一个参数顺序,而是源
在前,目在后。
在小模式和中模式下,前面4 个拷贝函数所接收的源和目指针都
只能是近指针,不能用来拷贝远数据段内的数组。函数movedata克服
了这个缺陷,它允许指定源和目的段地址和偏移量,它没有解决源和
目重叠的问题,也要求源参数在前,目参数在后。
Turbo C Tools中的函数utmovmem与Turbo C的movedata是类似的,
但它自动解决了源和目的重叠问题。 void utmovmem(const char far *psource,char far *ptarget,
unsigned int length);
用于内存之间比较的Turbo C函数有如下两个: int _Cdecl memcmp(const void *s1,const void *s2,size_t n);
int _Cdecl memicmp(const void *s1,const void *s2,size_t n); 这两个函数都是比较两个字节数组的前n 个字节,根据s1是小于、
等于还是大于s2,返回值分别为小于0、等于0和大于0。但函数memcmp
是精确比较,把每个字节看作无符号8位数,而函数memicmp把每个字
节看作一个字符,忽略大小写的差别。
用于内存设置的Turbo C函数有如下两个: void *_Cdecl memset(void *s,int c,size_t n);
void _Cdecl setmem(void *dest,unsigned length,char value);
这两个函数都是设置一块内存区域为某一个字节值,参数顺序不
一样,返回值也不一样,但实际作用看不出有什么区别。
用于从一个内存块的头n个字节中查找某一个字符的Turbo C函数
是memchr: void *_Cdecl memchr(const void *s,int c,size_t n); 如果找到了,则返回的指针向字符c第1次出现的位置。如果找不
到,则返回的指针为空。
▲评论
› 网友 王树坡 () 于 2013-08-06 15:21:33 发表评论说:
受教了,谢谢您!
我是王树坡,刚从学校毕业,打算从事计算机行业,能得到您的指点吗~~~
1121140449
我的QQ