萬萬沒想到,一個可執行文件原來包含了這麼多信息!

來源:公衆號【編程珠璣】

作者:守望先生

ID:shouwangxiansheng

 

拿到一個編譯好的可執行文件,你能獲取到哪些信息?文件大小,修改時間?文件類型?除此之外呢?實際上它包含了很多信息,這些你都知道嗎?

示例程序

//main.c
#include<stdio.h>
void testFun()
{
    printf("公衆號:編程珠璣\n");
}
int main(void)
{
    testFun();
    return 0;
}

編譯得到可執行文件main:

$ gcc -o main main.c

ELF頭信息

只需要一條簡單的命令,就可以獲取很多信息

$ readelf -h main
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400430
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6648 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 28

程序位數

Class:     ELF64

Class展示了該程序的位數,如這裏顯示的是ELF64,如果你將它放到一個32位系統中運行,運行得起來就怪了。換句話說,64位系統上能運行32位和64位的程序,但是32位系統上,無法運行64位的程序。

大小端

  Data:   2's complement, little endian

還記得那個到處可見的面試題嗎?如何判斷當前CPU是大端還是小端?除了各種秀代碼的方式,你想到這個方式了嗎?

找一個該平臺上的正運行的可執行文件或系統庫,然後使用readelf -h看一下,是不是很快就看出來了?多麼明顯的little endian。

關於大小端,更多內容可參考《談談字節序的問題》。

運行平臺

Machine:   Advanced Micro Devices X86-64

做嵌入式相關的可能經常需要做交叉編譯,而編譯出來的程序到底對不對呢?比如你在86平臺編譯arm的程序,最終生成的可執行文件到底能不能運行在arm平臺呢?通過Machine字段就可以很容易確定,從這裏可以看到,它是運行在x86平臺的。

同樣的,當你在交叉編譯的時候,發現總有一個庫鏈接不上,但是庫又存在,不妨看看這個庫和你要編譯的平臺是否匹配。

鏈接了哪些動態庫?

編好的程序依賴了哪些動態庫呢?可不要放到另外一個平臺就起不來啊。瞅瞅:

$ ldd main
    linux-vdso.so.1 =>  (0x00007ffe750e7000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f749920a000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f74995d4000)

原來鏈接了這些庫,所以當你在網上下載一些程序,運行的時候提示你某些so找不到,不妨看看它鏈接的動態庫在什麼位置,你的機器上到底有沒有吧。

新增的函數和全局變量包含了嗎?

新增了一個全局變量或者函數,但是編譯完之後,不確定有沒有?

$ nm main |grep testFun
0000000000400526 T testFun

nm看下就知道了。當然了,如果你看到某個庫的函數前面的標誌不是T,而是U,說明該函數未在該庫中定義。

nm主要用於查看elf文件的符號表信息。

有符號表嗎

我們都知道,沒有符號表的程序,在core之後是沒有太多有效信息可看的,也是無法使用gdb正常調試的,這個在《GDB調試入門,看這篇就夠了》中已經有提到了,那麼怎麼看有沒有符號表呢?

$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0d9a7eb860459b585d2b33ae28d7c67d5ba12669, not stripped

咦?你看這裏是不是也可以看到程序位數,適用平臺等信息?

如果使用file命令看到最後是not stripped,那麼則含有符號表,一般線上的程序可能會選擇去掉符號表信息,因爲可以大大減少可執行文件的空間佔用。

$ strip main

這個時候再看看:

$ nm main
no main symbols

程序佔用空間太大?

爲什麼程序的佔用空間這麼大?不妨看看是不是使用了過多的靜態變量或全局變量:

$ size main
   text       data     bss     dec     hex filename
   1261        552       8    1821     71d main

看到data部分的大小了嗎?看起來並沒有多少,如果這裏佔用空間過大,那可能是你程序中用到了太多的全局變量和靜態變量或常量。當然了,如果你的全局變量都是初始化爲0的,那麼data這裏是不會有明顯的變化的(爲什麼?)。

在開頭分別加下面這一行,其影響可執行文件的效果不一樣奧。

char str[1000] = {0};
char str[1000] = {1};

包含某個字符串嗎

這個程序裏面包含什麼特殊的字符串嗎?可以搜索一下:

$ strings main |grep hello
hello,

嗯?這樣一想,好像還可以把版本號信息寫進去呢。

C還是C++?

如果將前面的程序按照C++編譯:

$ g++ -o main main.c
$ nm main |grep test
0000000000400526 T _Z7testFunv

你會發現使用g++編譯出來的test函數符號前帶頭,後帶尾,這也是C++中有重載和C中沒有重載的原因之一。

函數的彙編代碼是?

反彙編所有代碼:

$ objdump -d main

那如果要反彙編特定函數(如main函數)呢?先按照地址順序輸出符號表信息:

$ nm -n main |grep main -A 1
0000000000400537 T main
0000000000400550 T __libc_csu_init

我們得到main的開始地址爲0x400537,結束地址爲0x400550。
反彙編:

$ objdump -d main --start-address=0x400537 --stop-address=0x400550
0000000000400537 <main>:
  400537:    55                      push   %rbp
  400538:    48 89 e5                mov    %rsp,%rbp
  40053b:    b8 00 00 00 00          mov    $0x0,%eax
  400540:    e8 e1 ff ff ff          callq  400526 <testFun>
  400545:    b8 00 00 00 00          mov    $0x0,%eax
  40054a:    5d                      pop    %rbp
  40054b:    c3                      retq   
  40054c:    0f 1f 40 00             nopl   0x0(%rax)

看看只讀數據區有哪些內容?

當我們嘗試修改常量字符串的時候,編譯器會提示我們,它們是隻讀的,真的如此嗎?

$ readelf main -x .rodata
Hex dump of section '.rodata':
  0x004005d0 01000200 00000000 68656c6c 6f2ce585 ........hello,..
  0x004005e0 ace4bc97 e58fb7ef bc9ae7bc 96e7a88b ................
  0x004005f0 e78fa0e7 8e9100                     .......

看到了嗎?我們的hello,字符串放在了這裏。

總結

本文僅列出了一些比較常見的可執行文中能讀到的信息,歡迎補充。

思考

對於a和b,它們的內存存儲區域是一樣的嗎?爲什麼?

char *a = "hello,world";
char a[] = "hello,world";

sizeof計算a和b的大小一樣嗎?又爲什麼?

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