程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C說話中的鏈接編寫教程

C說話中的鏈接編寫教程

編輯:關於C++

C說話中的鏈接編寫教程。本站提示廣大學習愛好者:(C說話中的鏈接編寫教程)文章只能為提供參考,不一定能成為您想要的結果。以下是C說話中的鏈接編寫教程正文


鏈接
  鏈接就是將分歧部門的代碼和數據搜集和組分解為一個單一文件的進程,這個文件可被加載或拷貝到存儲器履行.
  鏈接可以履行與編譯時(源代碼被翻譯成機械代碼時),也能夠履行與加載時(在法式被加載器加載到存儲器並履行時),乃至履行與運轉時,由運用法式來履行.在古代體系中,鏈接是由鏈接器主動履行的.
  鏈接器分為:靜態鏈接器和靜態鏈接器兩種.
靜態鏈接器
  靜態鏈接器以一組可重定位目的文件和敕令行參數作為輸出,生成一個完整鏈接的可以加載和運轉的可履行目的文件作為輸入.

  靜態鏈接器重要完成兩個義務:
  1>符號解析:目的文件界說和援用符號.符號解析的目標在於將每一個符號援用和一個符號界說接洽起來.
  2>重定位:編譯器和匯編器生成從地址零開端的代碼和數據節.鏈接器經由過程把每一個符號界說和一個存儲器地位接洽起來,然後修正一切對這些符號的援用,使得他們履行這個存儲地位,從而重定位這些節.

  目的文件:
  目的文件有三種情勢:
  1>可重定位的目的文件:
  包括二進制代碼和數據,其情勢可以再編譯時與其他可定位目的文件歸並起來,創立一個可履行目的文件.
  2>可履行目的文件:
  包括二進制代碼和數據,其情勢可以被直接拷貝到存儲器並履行.
  3>同享目的文件:
  一種特別的可重定位目的文件,可以再加載或運轉時,主動態地夾在到存儲器並履行.
  編譯器和匯編器生成可重定位目的文件(包含同享目的文件),鏈接器生成可履行目的文件.

  可重定位目的文件:
  EF頭L以一個16字節的序列開端,這個序列描寫了字的年夜小和生成該文件的體系字節次序.ELF頭剩下的部門包括贊助鏈接器解析息爭釋目的文件的信息.個中包含ELF頭的年夜小,目的文件的類型(好比,可重定位,可履行,同享目的文件),機械類型,節頭部表的文件偏移,和節頭部表中的表目年夜小和數目.分歧節的地位和年夜小是節頭部表描寫的,個中目的文件中的每一個節都有一個固定年夜小的表目.ELF格局的可重定位目的文件構造以下圖:

.text:已編譯法式的機械代碼
.rodata:只讀數據
.data:已初始化的全局C變量
.bss:未初始化的全局C變量.在目的文件中這個節不占現實空間,僅是一個占位符.
.sysmtab:一個符號表,寄存在法式中被界說和援用的函數和全局變量的信息.
.rel.text:當鏈接器把這個目的文件和其他文件聯合時,.text節中的很多地位都須要修正.普通而言,任何挪用內部函數或許援用全局變量的指令都要修正.另外一個方面,挪用當地函數的指令則不須要修正.
.rel.data:被模塊界說或援用的任何全局變量的信息.
.debug:一個調試符號表
.line:原始C源法式中的行號和.text節中機械指令之間的映照.
.strtab:一個字符串表,個中內容包含.symtab和.debug節中的符號表,和節頭部中的節名字.

 

  符號和符號表
  每一個可重定位目的模塊m都有一個符號表,它包括m所界說和援用的符號的信息.在鏈接器高低文中,有三種分歧的符號:
  1>由m界說並能被其他模塊援用的全局符號.全局鏈接器符號對應於非靜態的C函數和被界說為不帶C的static屬性的全局變量.
  2>由其他模塊界說並被模塊m援用的全局符號.這些符號成為內部符號,對應於界說在其他模塊中的C函數和變量.
  3>只被模塊m界說和援用的當地符號.有的當地符號鏈接器符號對應於帶static屬性的C函數和全局變量.這些符號在模塊m中的任何處所都可見,然則不克不及被其他模塊援用.目的文件中對應於模塊m的節和響應的源文件的名字也能取得當地符號.

  符號表式有匯編器結構的,應用編譯器輸入到匯編說話.s文件中的符號.sysmab節中包括ELF符號表.這張符號表包括一個關於表目標數組.表目標格局以下:

typedef struct{
 int name; //string table offset
 int value; //section offset, or VM address
 int size; //object size in bytes
 char type:4, //data, func, section, or src file
    binding:4; //local or global
 char reserved; //unused
 char section; //section header index, ABS, UNDEF, or COMMON
}Elf_Symbol;

符號解析
  鏈接器解析符號援用的辦法是將每一個援用和它輸出的可重定位目的文件按的符號表中的一個肯定的符號界說接洽起來.
  關於那些和援用界說在雷同模塊的當地符號的援用,符號解析式異常簡略清楚明了的.編譯器只許可每一個模塊中的每一個當地符號只要一個界說.編譯器還確保靜態當地變量,它們會有當地鏈接器符號,具有獨一的名字.
  關於全局符號的援用解析,當編譯器碰到一個不是在以後模塊中界說的符號(變量或函數名)時,它會假定該符號式在其他某個模塊中界說的,生成一個鏈接器符號表表目,並把它交給鏈接器處置.假如鏈接器在它的任何輸出模塊中都找不到這個被援用的符號,它就輸入一條毛病信息並終止.
  在編譯時,編譯器輸入的每一個全局符號給匯編器,或許是強,或許是弱,而匯編器把這個信息隱含地編碼在可重定位目的文件的符號表中.函數和以初始化的全局變量是強符號,未初始化的全局變量是弱符號.
  依據符號的強弱,有以下規矩:
  1>不許可有多個強符號
  2>假如有一個強符號和多個弱符號,則選擇強符號
  3>假如有多個弱符號,則任選一個弱符號

  與靜態庫鏈接
  一切編譯體系都供給一種機制,將一切相干的目的模塊打包為一個零丁的文件,稱為靜態庫,它可以用做鏈接器的輸出.當鏈接器結構一個輸入的可履行文件時,它只拷貝靜態庫裡被運用法式援用的目的模塊.
  在unix體系中,靜態庫以一種稱為存檔的特別文件格局寄存在磁盤中.存檔文件是一組銜接起來的可重定位目的文件的聚集,有一個頭部描寫每一個成員目的文件的年夜小和地位.

  鏈接器若何應用靜態庫來解析援用
  在符號解析階段,鏈接器從左到右依照它們在編譯驅動法式敕令行上湧現的雷同次序來掃描可重定位目的文件和存檔文件.在此次掃描中,鏈接器地位一個可重定位目的文件聚集E,這個聚集中的文件會被歸並起來構成可履行文件,和一個未解析的符號聚集U,和一個在後面輸出文件中已界說的符號聯合D.初始時,E,U,D都是空的.
  1>關於敕令行上的每一個輸出文件f,鏈接器會斷定f是一個目的文件照樣一個存檔文件.假如是一個目的文件,那末鏈接器把f添加到E,修正U和D來反應f中的符號界說和援用,並持續下一個輸出文件.
  2>假如f是一個存檔文件,那末鏈接器就測驗考試婚配U中未解析的符號由存檔文件成員界說的符號.假如某個存檔文件成員m,界說了一個符號來解析U中的一個援用,那末就將m加到E中,而且鏈接器修正U和D來反應m中的符號界說和援用.對存檔文件中的一切成員目的文件都重復停止這個進程,曉得U和D都不再產生變更.在此時,任何不包括在E中的成員目的文件都邑被拋棄,而鏈接器將持續到下一個輸出文件.
  3>假如當鏈接器完成對輸出敕令行的掃描後,U長短空的,那末鏈接器就會輸入一個毛病並終止.不然,它匯合偏重定位E中的目的文件,從而構建輸入的可履行文件.

  這類方法,招致了在輸出敕令時要斟酌到,靜態庫和目的文件的地位,庫文件放在目的文件的前面,假如庫文件之間有援用關系,則被援用的庫放在前面.

重定位
  當鏈接器完成了符號解析這一步時,它就把代碼中的每一個符號援用和肯定的一個符號界說(也就是,它的一個輸出目的模塊中的一個符號表表目)接洽起來.此時,鏈接器就曉得它的輸出目的模塊中的代碼節和數據解切實其實切年夜小.然後就開端重定位步調.重定位由兩步構成:
  1>重定位節和符號界說:
  在這一步中,鏈接器將一切雷同類型的節歸並為一個新的聚合節.然後,鏈接器將運轉時存儲器地址賦值給新的聚合節,賦給輸出模塊界說的每一個節,和賦給輸出模塊界說的每一個符號.當這一步完成時,法式中的每一個指令和全局變量都一個獨一的運轉時存儲器地址.
  2>重定位節中的符號援用:
  在這一步中,鏈接器修正代碼節和數據節中對每一個符號的援用,使得它們指向准確的運轉時地址.為了履行這一步,鏈接器依附於稱為重定位表目標可重定位目的模塊中的數據構造.

  重定位表目:
  當匯編器生成一個目的模塊時,它其實不曉得數據和代碼終究將寄存在存儲器中的甚麼地位.它也不曉得這個模塊援用的任何內部界說的函數或許全局變量的地位.所以,不管什麼時候匯編器碰到對終究地位未知的目的援用,它就會生成一個重定位表目,告知鏈接器在將目的文件歸並為可履行文件時,若何修正這個援用.代碼的重定位表目放在.rel.text中.已初始化數據的重定位表目放在rel.data中.
  ELF重定位表目標格局以下:
  typedef struct{
    int offset;  //offset of the reference to relocate
    int symbol:24,  //symbol the reference point to
        type:8;  //relocation type
  } Elf32_Rel;

  ELF界說了11中分歧的重定位類型,個中最根本的兩種重定位類型是:R_386_PC32(重定位一個應用32PC相干的地址援用)和R_386_32(重定位一個應用32位相對地址的援用).

靜態鏈接器
  同享庫是一個目的模塊,在運轉時,可以加載就任意的存儲器地址,並在存儲器中和一個法式鏈接起來.這個進程稱為靜態鏈接,是由靜態鏈接器完成的.
  同享庫的同享在兩個方面有所分歧.起首,在任何給定的文件體系中,關於一個庫只要一個.so文件.一切援用該庫德可履行目的文件同享這個.so文件中的代碼和數據,而不是像靜態庫德內容那樣被拷貝和嵌入到援用它們的可履行的文件中.其次,在存儲器中,一個同享庫的.text節只要一個正本可以被分歧的正在運轉的過程同享.

  多目的文件的鏈接
stack.c

   

#include <stdio.h> 
   
  #define STACKSIZE 1000 
   
  typedef struct stack { 
    int data[STACKSIZE]; 
    int top; 
  } stack; 
   
  stack s; 
  int count = 0; 
   
  void pushStack(int d) 
  { 
    s.data[s.top ++] = d; 
    count ++; 
  } 
   
  int popStack() 
  { 
    return s.data[-- s.top]; 
  } 
   
  int isEmpty() 
  { 
    return s.top == 0; 
  } 


link.c

 

  #include <stdio.h> 
   
  int a, b; 
   
  int main() 
  { 
    a = b = 1; 
   
    pushStack(a); 
    pushStack(b); 
    pushStack(a); 
   
    while (! isEmpty()) { 
      printf("%d\n", popStack()); 
    } 
     
    return 0; 
  } 


編譯方法:

gcc -Wall stack.c link.c -o main

提醒失足信息以下:

然則代碼是可以履行的

界說和聲明

static和extern潤飾函數
上述編譯湧現毛病的緣由是:編譯器在處置函數挪用代碼時沒有找到函數原型,只好依據函數挪用代碼做隱式聲明,把這三個函數聲明為:

  int pushStack(int); 
  int popStack(void); 
  int isEmpty(void); 


編譯器常常不曉得去哪裡找函數界說,像下面的例子,我讓編譯器編譯main.c,而這幾個函數界說卻在stack.c裡,編譯器沒法曉得,是以可以用extern聲明。修正link.c以下:

 

  #include <stdio.h> 
   
  int a, b; 
   
  extern void pushStack(int d); 
  extern int popStack(void); 
  extern int isEmpty(void); 
   
  int main() 
  { 
    a = b = 1; 
   
    pushStack(a); 
    pushStack(b); 
    pushStack(a); 
   
    while (! isEmpty()) { 
      printf("%d\n", popStack()); 
    } 
     
    return 0; 
  } 


如許編譯器就不會報警了。這裡extern症結字表現這個標識符具有External Linkage.pushStack這個標識符具有External Linkage指的是:假如link.c和stack.c鏈接在一路,假如pushStack在link.c和stack.c中都聲明(在stack.c中的聲明同時也是界說),那末這些聲明指的是統一個函數,鏈接後是統一個GLOBAL符號,代表統一個地址。函數聲明中的extern可以省略不寫,不屑extern的函數聲明也表現這個函數具有External Linkage。

假如用static症結字潤飾一個函數聲明,則表現該標識符具有Internal Linkage,例若有以下兩個法式文件:

  /* foo.c */ 
   
  static void foo(void) {} 


  /*main.c*/ 
   
  void foo(void); 
   
  int main(void) { foo(); return 0;} 


編譯鏈接在一路會失足,緣由是:

固然在foo.c中界說了函數foo,然則這個函數是static屬性,只具有internal Linkage。假如把foo.c編譯成目的文件,函數名foo在個中是一個LOCAL的符號,不介入鏈接進程,所以在鏈接時,main.c頂用到一個External Linkage的foo函數,鏈接器卻找不到它的界說在哪,沒法肯定它的地址,也就沒法做符號解析,只好報錯。

但凡被屢次聲明的變量或函數,必需有且只要一個聲明是界說,假如有多個界說,或許一個界說都沒有,鏈接器就沒法完成鏈接


static和extern潤飾變量
假如我想在link.c中拜訪stack.c中界說的int變量count,則可以用extern聲明

  

 #include <stdio.h> 
   
  int a, b; 
   
  extern void pushStack(int d); 
  extern int popStack(void); 
  extern int isEmpty(void); 
  extern int count; 
   
  int main() 
  { 
    a = b = 1; 
   
    pushStack(a); 
    pushStack(b); 
    pushStack(a); 
   
    printf("%d\n", count); 
   
    while (! isEmpty()) { 
      printf("%d\n", popStack()); 
    } 
     
    return 0; 
  } 


變量count具有external linkage,它的存儲空間是在stack.c平分配的,所以link.c中的變量聲明extern int count;不是變量界說,由於它不分派存儲空間。

假如不想在stack.c外讓外界拜訪到count,則可以用static症結字將count聲明為Internal Linkage
差別
變量性命和函數聲明有一點分歧,函數聲明的extern可寫可不寫,而變量聲明假如不寫extern,意思就完整變了。假如下面的例子不寫extern就表現在main函數中界說一個全局變量count。

用static症結字聲明具有Internal Linkage的函數和症結字是處於掩護外部狀況的目標,也是一種封裝(Encapsulation)的思惟。一個模塊中,有些函數是供給給外界應用的,也稱為導出(Export)給外界應用,這些函數用extern聲明為External Linkage的。


頭文件
為了避免每次函數extern聲明,例如又有一個foo.c也應用pushStack等函數,又須要在foo.c中寫多個extern聲明,為了不這類反復費事的操作,可以本身界說一個stack.h頭文件:

  

 #ifndef STACK_H 
  #define STACK_H 
   
  #define STACKSIZE 1000 
   
  typedef struct stack { 
    int data[STACKSIZE]; 
    int top; 
  } stack; 
   
  extern void pushStack(int d); 
  extern int popStack(void); 
  extern int isEmpty(void); 
   
  #endif 


如許,在link.c裡就只須要包括這個頭文件便可以了,而不須要寫三個函數聲清楚明了:

   

 #include <stdio.h> 
  #include "stack.h" 
   
  int a, b; 
   
  extern int count; 
   
  int main() 
  { 
    a = b = 1; 
   
    pushStack(a); 
    pushStack(b); 
    pushStack(a); 
   
    printf("%d\n", count); 
   
    while (! isEmpty()) { 
      printf("%d\n", popStack()); 
    } 
     
    return 0; 
  } 


為何#include <stdio.h>用角括號,而#include "stack.h"用引號?緣由:

  •     關於用角括號包括的頭文件,gcc起首查找-I選項指定的目次,然後查找體系的頭文件目次(平日是/usr/include)
  •     關於用“”包括的頭文件,gcc起首查找包括頭文件的.c文件地點的目次,然後查找-I選項指定的目次,然後查找體系的頭文件目次


用#ifndef #define #endif是為了避免頭文件的反復包括,頭文件反復包括的成績以下:

  •     使預處置的速度變慢了,要處置許多原來不須要處置的頭文件
  •     假如a.h包括了b.h,然後b.h又包括了a.h的情形,預處置就墮入逝世輪回了
  •     頭文件按有些代碼不許可反復湧現


頭文件中的變量和函數聲明必定不克不及是界說。假如頭文件中湧現變量或函數界說,這個頭文件又被多個.c文件包括,那末這些.c文件就不克不及鏈接在一路

 

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved