程序必定要從main函數開始運行嗎?

程序必定要從main函數開始運行嗎?本文涉及靜態連接相關知識。linux

對於靜態連接先提出兩個問題:c++

Q:每一個目標文件都有好多個段,目標文件在被連接成可執行文件時,輸入目標文件中的各個段如何被合併到輸出文件?程序員

A:合併類似的段,將全部的.text段合併到輸出文件的.text段,將全部的.data段合併到輸出文件的.data段。編程

Q:連接器如何爲他們分配在輸出文件中的空間和地址?架構

A:這裏涉及到程序連接的兩個步驟:ide

  1. 空間與地址分配:掃描全部的輸入目標文件,得到它們每一個段的長度屬性和位置,收集輸入目標文件中的符號表中的全部符號定義和符號引用,統一放到一個全局符號表中,合併全部的段,計算出輸出文件中各個段合併後的長度和位置,並創建映射關係。
  2. 符號解析與重定位:使用第一步收集到的全部信息,讀取輸入文件中段的數據及重定位信息,進行符號解析和重定位,調整代碼中的地址,將每一個段中須要重定位的指令和數據進行「修補」,使他們都指向正確的位置。

tips:外部符號指的是目標文件須要引用的符號,可是定義在其它目標文件中,連接前外部符號地址都是000000之類,連接後的可執行文件就能夠看見這些外部符號都是有地址的。連接就是把類似的段放在一塊兒,先找到段的偏移地址,再找出符號在段中的偏移,這樣能夠肯定符號在整個可執行程序中的地址。函數

對於那些須要重定位的符號,都會放在重定位表裏,也叫重定位段,即.rel.data、.rel.text等,若是.text段有被重定位的地方,就有.rel.text段,若是.data段有被重定位的地方,就有.rel.data段。可使用objdump查看目標文件的重定位表。工具

源代碼:優化

int main() {
    printf("程序喵\n");
    return 0;
}
gcc -c test
objdump -r test.o

test.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000007 R_X86_64_PC32     .rodata-0x0000000000000004
000000000000000c R_X86_64_PLT32    puts-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE
0000000000000020 R_X86_64_PC32     .text

使用nm也能夠查看須要重定位的符號:ui

nm -u test.o
                 U _GLOBAL_OFFSET_TABLE_
                 U puts

對於UND類型,這種未定義的符號都是由於該目標文件中有關於他們的重定位項,在連接器掃描完全部的輸入目標文件後,全部這種未定義的符號都應該能在全局符號表中找到,不然報符號未定義錯誤。

注意:咱們代碼裏明明用的是printf,爲何它卻引用了puts的符號呢,由於編譯器默認狀況下會把只用一個字符串參數的printf替換成puts, 能夠節省格式解析的時間,使用-fno-builtin會關閉這個內置函數優化選項,以下:

~/test$ gcc -c -fno-builtin testlink.cc -o test.o
~/test$ nm test.o
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
                 U printf

tips:如今的程序和庫一般來說都很大,一個目標文件可能包含成百上千個函數或變量,當須要用到某個目標文件的任意一個函數或變量時,就須要把它整個目標文件都連接進來,也就是說那些沒有用到的函數也會被連接進去,這會致使連接輸出文件變的很大,形成空間浪費。

有一個編譯選項叫函數級別連接,可使得某個函數或變量單獨保存在一個段裏面,都連接器須要用到某個函數時,就將它合併到輸出文件中,對於沒用到的函數則將他們拋棄,減小空間浪費,但這會減慢編譯和連接過程,GCC編譯器的編譯選項是:

-ffunction-sections
-fdata-sections

可能不少人都會覺得程序都是由main函數開始執行和結束的,但其實不是,在main函數調用以前,爲了保證程序能夠順利進行,要先初始化進程執行環境,如堆分配初始化、線程子系統等,C++的全局對象構造函數也是這一時期被執行的,全局析構函數是main以後執行的。

Linux通常程序的入口是__start函數,有兩個段:

  • .init段:進程的初始化代碼,一個程序開始運行時,在main函數調用以前,會先運行.init段中的代碼。
  • .fini段:進程終止代碼,當main函數正常退出後,glibc會安排執行該段代碼。
如何指定程序入口

在ld連接過程當中使用-e參數能夠指定程序入口,因爲一段簡短的printf函數其實都依賴了好多個連接庫,咱們也不太方便使用連接腳本將目標文件與全部這些依賴庫進行連接,因此使用下面這段內嵌彙編的程序來打印一段字符串,這段程序不依賴任何連接庫就能夠打印出字符串內容,讀者若是不懂其中的含義也不用擔憂,只須要了解下面介紹的連接知識就好。

代碼以下:

const char* str = "hello";

void print() {
    asm("movl $13,%%edx \n\t"
        "movl str,%%ecx \n\t"
        "movl $0,%%ebx \n\t"
        "movl $4,%%eax \n\t"
        "int $0x80 \n\t"
        :
        :"r"(str):"edx", "ecx", "ebx");
}


void exit() {
    asm("movl $42,%ebx \n\t"
        "movl $1,%eax \n\t"
        "int $0x80 \n\t");
}

void nomain() {
    print();
    exit();
}

使用以下命令生成目標文件:

gcc -c -fno-builtin test.cc

看下輸出的test.o的符號:

~/test$ nm -a test.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 d .data.rel.local
0000000000000000 r .eh_frame
0000000000000000 n .note.GNU-stack
0000000000000000 r .rodata
0000000000000000 t .text
0000000000000026 T _Z4exitv
0000000000000000 T _Z5printv
0000000000000039 T _Z6nomainv
0000000000000000 D str
0000000000000000 a test.cc

這裏因爲個人源文件是.cc結尾,因此是以c++方式編譯的,因此符號變成了上面的形式,若是變成了test.c,符號以下:

~/test$ gcc -c -fno-builtin test.c -o test.o
~/test$ nm -a test.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 d .data.rel.local
0000000000000000 r .eh_frame
0000000000000000 n .note.GNU-stack
0000000000000000 r .rodata
0000000000000000 t .text
0000000000000026 T exit
0000000000000039 T nomain
0000000000000000 T print
0000000000000000 D str
0000000000000000 a test.c

再使用-e指定入口函數符號:

~/test$ ld -static -e nomain -o test test.o
~/test$ ./test
hello
如何使用自定義連接腳本實現自定義段的功能

在ld連接過程當中使用-T參數能夠指定連接腳本,經過ld -verbose能夠查看默認的連接腳本,原文太長,這裏簡單截取了一部分:

$ ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.30
  Supported emulations:
   elf_x86_64
   elf32_x86_64
   elf_i386
   elf_iamcu
   i386linux
   elf_l1om
   elf_k1om
   i386pep
   i386pe
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2018 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
              "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
 
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : { *(.plt) *(.iplt) }
  .plt.got        : { *(.plt.got) }
  .plt.sec        : { *(.plt.sec) }
  .text           :
  {
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf32.em.  */
    *(.gnu.warning)
  }
  .fini           :
  {
    KEEP (*(SORT_NONE(.fini)))
  }
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
  /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}

這裏自定義一個簡單的連接腳本test.lds

ENTRY(nomain)

SECTIONS
{
    . = 0x8048000 + SIZEOF_HEADERS;
    tinytext : { *(.text) *(.data) *(.rodata) }
    /DISCARD/ : { *(.comment) }
}

再使用-T指定連接腳本:

~/test$ ld -static -T test.lds -e nomain -o test test.o
~/test$ ./test
hello

上面的tinytext一行是指將.text段、.data段、.rodata段的內容都合併到tinytext段中,使用readelf查看段的信息。

~/test$ readelf -S test
~/test$ There are 6 section headers, starting at offset 0x482a0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .eh_frame         PROGBITS         00000000080480b0  000480b0
       0000000000000078  0000000000000000   A       0     0     8
  [ 2] tinytext          PROGBITS         0000000008048128  00048128
       0000000000000066  0000000000000000 WAX       0     0     8
  [ 3] .shstrtab         STRTAB           0000000000000000  0004826e
       000000000000002e  0000000000000000           0     0     1
  [ 4] .symtab           SYMTAB           0000000000000000  00048190
       00000000000000c0  0000000000000018           5     4     8
  [ 5] .strtab           STRTAB           0000000000000000  00048250
       000000000000001e  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

工具小貼士

關於靜態連接庫:

ar rcs libxxx.a xx1.o xx2.o 打包靜態連接庫
ar -t libc.a 查看靜態連接庫裏都有什麼目標文件
ar -x libc.a 會解壓全部的目標文件到當前目錄
gcc --verbose 能夠查看整個編譯連接步驟

關於objdump:

objdump -i 查看本機目標架構
objdump -f 顯示文件頭信息
objdump -d 反彙編程序
objdump -t 顯示符號表入口,每一個目標文件都有什麼符號
objdump -r 顯示文件的重定位入口,重定位表
objdump -x 顯示全部可用的頭信息,等於-a -f -h -r -t
objdump -H 幫助

關於分析ELF文件格式:

readelf -h 列出文件頭
readelf -S 列出每一個段
readelf -r 列出重定位表
readelf -d 列出動態段

關於查看目標文件符號信息:

nm -a 顯示全部的符號
nm -D 顯示動態符號
nm -u 僅顯示沒有定義的外部符號
nm -defined-only 僅顯示定義的符號

關於符號的說明:

若是符號類型是小寫的,代表符號是局部符號,大寫表示符號是全局符號。

  • A:該符號的值是絕對的,在之後的連接過程當中,不容許進行改變。這樣的符號值,經常出如今中斷向量表中,例如用符號來表示各個中斷向量函數在中斷向量表中的位置。
  • B:該符號的值出如今.bss段中,未初始化的全局和靜態變量。
  • C:該符號的值在COMMON段中,裏面的都是弱符號。
  • D:該符號位於數據段中。
  • I:該符號對另外一個符號的間接引用
  • N:debug符號
  • R:該符號位於只讀數據區
  • T:該符號位於代碼段
  • U:該符號在當前文件未定義,定義在別的文件中
  • ?:該符號類型沒有定義

參考資料

https://linuxtools-rst.readth...

《程序員的自我修養》更多文章,請關注個人V X 公 主 號:程序喵大人,歡迎交流。

相關文章
相關標籤/搜索