六種編譯模式概述

 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 <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在後。

  小模式
  小模式是常用的模式,本書中大部分例子都是用小模式編譯的。雖然小模式與微模式一樣,都是小數據、小程序模式,但它與微模式有兩點重要的差別。第一,代碼和數據/堆棧/堆段是分開的,所以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 <stdio.h>
  #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<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=%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次出現的位置。如果找不到,則返回的指針爲空。

       轉自:http://blog.csdn.net/daishengs/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章