六种编译模式概述

来源:百度文库 编辑:神马文学网 时间:2024/04/27 16:22:58
 
 | 
六种编译模式概述
Turbo C 提供了六种编译模式。编译模式有时也称为寻址模式或内存模式,因为它处理的就是如何在内存中为程序,数据,堆栈分配空间并存取它们,这六种模式是:微模式tiny,小模式small, 紧凑模式compact,中模式medium,大模式large,巨模式huge。它们之间的关系如下表所示。
│ 小程序   │ 大程序
━━━━┿━━━━━━┿━━━━━━━━
小数据 │ 微,小   │ 中
大数据 │ 紧凑    │ 大,巨
所谓小程序就是只有一个程序段,当然不超过64K 字节,缺省的码(函数)指针是near。所谓大程序就是有多个程序段,每个程序段不超过64K字节,但总程序量可超过64K字节,缺省的码指针是 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
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
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在后。
小模式
小模式是常用的模式,本书中大部分例子都是用小模式编译的。虽然小模式与微模式一样,都是小数据、小程序模式,但它与微模式有两点重要的差别。第一,代码和数据/堆栈/堆段是分开的,所以CS不等于DS和SS。第二,除了和数据/堆栈共用一个段的堆外,还有一个远堆,以far指针进行存取。从数据/堆栈段的末尾直到常规内存的末尾都是属于远堆。因为代码、静态数据和(近)堆仍然在同一个段内,所以小模式下缺省的数据指针和函数指针都是near。结果,在小模式下不能直接通过该模式下的Turbo C 函数来处理远堆中的变量。然而,只要程序提供自己的操作函数,就可以存取整个远堆中的任一单元,即可以使用整个常规内存。
紧凑模式
紧凑模式在概念上是最简单的,代码、静态数据、堆栈、堆各有其自己的段。堆只有远堆,没有近堆。像小模式和中模式中的远堆一样,堆是用far指针来存取的。可以用Turbo C的库函数来处理堆变量。所有数据指针都是far,函数指针都是near。从演示程序的输出中可以看出,CS、DS、SS三个寄存器的值彼此不等。值得注意的是,静态数据的总量仍不可超过64K字节。
中模式
在数据/堆栈/堆的分配方面,中模式与小模式是一样的,差别在于码段的分配。在中模式下,来自不同源文件的码模式放在不同的码段内。严格地讲,同一源文件内的各函数也是放在不同代码段内。各码段的总空间数只受微机上可用内存的限制。因为有多个码段,所以Turbo C必须用far函数指针。在演示程序输出的结果中函数a 的地址为74F9:000E,函数main的地址为74FE:0004。函数a 的地址较低,是因为在连接时包含函数a的x.obj在前。在中模式下,堆仍然有近堆和远堆之分。
大模式
在静态数据/堆栈/堆的分配方面,大模式等同于紧凑模式。在代码的分配方面,大模式等同于中模式。无论是数据指针还是函数指针,一律都是远指针。与紧凑模式一样,静态数据的总量不可超过64K字节。
巨模式
巨模式取消了静态数据总量不可超过64K字节的限制。来自不同源文件的代码放在不同的段内,来自不同源文件的静态数据也是放在不同的段内,只有堆栈是合在一起的。以前举的例子就是利用了这一特点。从演示程序的输出中也可以看,当从函数main内调用函数a 时,不但CS改变了,DS也改变了。当然,两个函数共用了同一个堆栈,否则就无法正确返回。应该注意的是,不要把巨模式和巨指针混为一谈,在巨模式下缺省的指针仍是far而不是huge。
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
堆栈的组织
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
#define report printf("coreleft=%un",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
void main()
{
int i,n=9000;
double huge *a;
double sum;
a=(double huge *)farcalloc(n,sizeof(double));
for(i=0; ifor(i=0,sum=0; iprintf("a[i]=i for i=0。n-1; n=%dn",n);
printf("sum of all a[i]=%8.0fn",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次出现的位置。如果找不到,则返回的指针为空。