六种编译模式概述 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
"); printf(" CS : %X
",_CS); printf(" DS : %X
",_DS); printf(" SS : %X
",_SS); printf(" Static B : %p
",&b); pritnf(" Automatic C : %p
",&c); } /* Y.C */ #include<general.h> int d; void main(){ int e; a(); printf("In function main
"); printf(" CS : %X
",_CS); pritnf(" DS : %X
",_DS); pritnf(" SS : %X
",_SS); pritnf(" Global D : %p
",&d); pritnf(" Automatic E : %p
",&e); printf(" Heap address : %p
",malloc(2)); #if defined(__TINY__)||defined(__SMALL__)||defined(__COMPACT__) pritnf("Function A : %Np
",a); pritnf("Function main : %Np
",main); } #else printf("Function A : %Fp
",a); printf("Function main : %Fp
",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
",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 for i=0。 n-1; n=%d
",n); printf("sum of all a=%8.0f
",sum); printf("(n-1)n/2 =%1d
",(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次出现的位置。如果找不 到,则返回的指针为空。 |