linux c 内存elf,关于LINUX下的可执行程序ELF(一)
★概要: 這片文檔從程序員的角度討論了linux的ELF二進制格式。介紹了一些ELF執行
文件在運行控制的技術。展示了如何使用動態連接器和如何動態裝載ELF。
我們也演示了如何在LINUX使用GNU C/C++編譯器和一些其他工具來創建共享的
C/C++庫。★1前言最初,UNIX系統實驗室(USL)開發和發布了Executable and linking Format
(ELF)這樣的二進制格式。在SVR4和Solaris 2.x上,都做為可執行文件默認的
二進制格式。ELF比a.out和COFF更強大更靈活。結合一些適當的工具,程序員
使用ELF就可以在運行時控制程序的流程。
★2 ELF類型三種主要的ELF文件類型:.可執行文件:包含了代碼和數據。具有可執行的程序。
例如這樣一個程序
# file dltest
dltest: ELF 32-bit LSB executable, Intel 80386, version 1,
dynamically linked (uses shared libs), not stripped.可重定位文件:包含了代碼和數據(這些數據是和其他重定位文件和共享的
object文件一起連接時使用的)
例如這樣文件 # file libfoo.o
libfoo.o: ELF 32-bit LSB relocatable, Intel 80386, version 1,
not stripped.共享object文件(又可叫做共享庫):包含了代碼和數據(這些數據是在連接
時候被連接器ld和運行時動態連接器使用的)。動態連接器可能稱為
ld.so.1,libc.so.1 或者 ld-linux.so.1。
例如這樣文件
# file libfoo.so
libfoo.so: ELF 32-bit LSB shared object, Intel 80386, version
1, not strippedELF section部分是非常有用的。使用一些正確的工具和技術,程序員就能
熟練的操作可執行文件的執行。★3 .init和.fini sections在ELF系統上,一個程序是由可執行文件或者還加上一些共享object文件組成。
為了執行這樣的程序,系統使用那些文件創建進程的內存映象。進程映象
有一些段(segment),包含了可執行指令,數據,等等。為了使一個ELF文件
裝載到內存,必須有一個program header(該program header是一個描述段
信息的結構數組和一些為程序運行準備的信息)。一個段可能有多個section組成.這些section在程序員角度來看更顯的重要。每個可執行文件或者是共享object文件一般包含一個section table,該表
是描述ELF文件里sections的結構數組。這里有幾個在ELF文檔中定義的比較
特別的sections.以下這些是對程序特別有用的:.fini
該section保存著進程終止代碼指令。因此,當一個程序正常退出時,
系統安排執行這個section的中的代碼。
.init
該section保存著可執行指令,它構成了進程的初始化代碼。
因此,當一個程序開始運行時,在main函數被調用之前(c語言稱為
main),系統安排執行這個section的中的代碼。.init和.fini sections的存在有著特別的目的。假如一個函數放到
.init section,在main函數執行前系統就會執行它。同理,假如一
個函數放到.fini section,在main函數返回后該函數就會執行。
該特性被C++編譯器使用,完成全局的構造和析構函數功能。當ELF可執行文件被執行,系統將在把控制權交給可執行文件前裝載所以相關
的共享object文件。構造正確的.init和.fini sections,構造函數和析構函數
將以正確的次序被調用。★3.1 在c++中全局的構造函數和析構函數在c++中全局的構造函數和析構函數必須非常小心的處理碰到的語言規范問題。
構造函數必須在main函數之前被調用。析構函數必須在main函數返回之后
被調用。例如,除了一般的兩個輔助啟動文件crti.o和crtn.o外,GNU C/C++
編譯器--gcc還提供兩個輔助啟動文件一個稱為crtbegin.o,還有一個被稱為
crtend.o。結合.ctors和.dtors兩個section,c++全局的構造函數和析構函數
能以運行時最小的負載,正確的順序執行。
.ctors
該section保存著程序的全局的構造函數的指針數組。.dtors
該section保存著程序的全局的析構函數的指針數組。 ctrbegin.o
有四個section:
1 .ctors section
local標號__CTOR_LIST__指向全局構造函數的指針數組頭。在
ctrbegin.o中的該數組只有一個dummy元素。 [譯注:
# objdump -s -j .ctors
/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/crtbegin.o:
file format elf32-i386
Contents of section .ctors:
0000 ffffffff ....
這里說的dummy元素應該就是指的是ffffffff
] 2 .dtors section
local標號__DTOR_LIST__指向全局析構函數的指針數組頭。在
ctrbegin.o中的該數組僅有也只有一個dummy元素。 3 .text section
只包含了__do_global_dtors_aux函數,該函數遍歷__DTOR_LIST__
列表,調用列表中的每個析構函數。
函數如下:Disassembly of section .text:00000000 <__do_global_dtors_aux>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 3d 04 00 00 00 00 cmpl $0x0,0x4
a: 75 38 jne 44 <__do_global_dtors_aux>
c: eb 0f jmp 1d <__do_global_dtors_aux>
e: 89 f6 mov %esi,%esi
10: 8d 50 04 lea 0x4(%eax),%edx
13: 89 15 00 00 00 00 mov %edx,0x0
19: 8b 00 mov (%eax),%eax
1b: ff d0 call *%eax
1d: a1 00 00 00 00 mov 0x0,%eax
22: 83 38 00 cmpl $0x0,(%eax)
25: 75 e9 jne 10 <__do_global_dtors_aux>
27: b8 00 00 00 00 mov $0x0,%eax
2c: 85 c0 test %eax,%eax
2e: 74 0a je 3a <__do_global_dtors_aux>
30: 68 00 00 00 00 push $0x0
35: e8 fc ff ff ff call 36 <__do_global_dtors_aux>
3a: c7 05 04 00 00 00 01 movl $0x1,0x4
41: 00 00 00
44: c9 leave
45: c3 ret
46: 89 f6 mov %esi,%esi
4 .fini section
它只包含一個__do_global_dtors_aux的函數調用。請記住,它僅是
一個函數調用而不返回的,因為crtbegin.o的.fini section是這個
函數體的一部分。
函數如下:
Disassembly of section .fini:00000000 <.fini>:
0: e8 fc ff ff ff call 1 <.fini>
crtend.o
也有四個section: 1 .ctors section
local標號__CTOR_END__指向全局構造函數的指針數組尾部。 2 .dtors section
local標號__DTOR_END__指向全局析構函數的指針數組尾部。 3 .text section
只包含了__do_global_ctors_aux函數,該函數遍歷__CTOR_LIST__
列表,調用列表中的每個構造函數。
函數如下:
00000000 <__do_global_ctors_aux>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 53 push %ebx
4: bb fc ff ff ff mov $0xfffffffc,%ebx
9: 83 3d fc ff ff ff ff cmpl $0xffffffff,0xfffffffc
10: 74 0c je 1e <__do_global_ctors_aux>
12: 8b 03 mov (%ebx),%eax
14: ff d0 call *%eax
16: 83 c3 fc add $0xfffffffc,%ebx
19: 83 3b ff cmpl $0xffffffff,(%ebx)
1c: 75 f4 jne 12 <__do_global_ctors_aux>
1e: 8b 5d fc mov 0xfffffffc(%ebp),%ebx
21: c9 leave
22: c3 ret
23: 90 nop 4 .init section
它只包含一個__do_global_ctors_aux的函數調用。請記住,它僅是
一個函數調用而不返回的,因為crtend.o的.init section是這個函
數體的一部分。
函數如下:
Disassembly of section .init:00000000 <.init>:
0: e8 fc ff ff ff call 1 <.init>
crti.o
在.init section中僅是個_init的函數標號。
在.fini section中的_fini函數標號。crtn.o
在.init和.fini section中僅是返回指令。Disassembly of section .init:00000000 <.init>:
0: 8b 5d fc mov 0xfffffffc(%ebp),%ebx
3: c9 leave
4: c3 ret
Disassembly of section .fini:00000000 <.fini>:
0: 8b 5d fc mov 0xfffffffc(%ebp),%ebx
3: c9 leave
4: c3 ret編譯產生可重定位文件時,gcc把每個全局構造函數掛在__CTOR_LIST上
(通過把指向構造函數的指針放到.ctors section中)。
它也把每個全局析構函掛在__DTOR_LIST上(通過把指向析構函的指針
放到.dtors section中)。連接時,gcc在所有重定位文件前處理crtbegin.o,在所有重定位文件后處理
crtend.o。另外,crti.o在crtbegin.o之前被處理,crtn.o在crtend.o之后
被處理。當產生可執行文件時,連接器ld分別的連接所有可重定位文件的ctors 和
.dtors section到__CTOR_LIST__和__DTOR_LIST__列表中。.init section
由所有的可重定位文件中_init函數組成。.fini由_fini函數組成。運行時,系統將在main函數之前執行_init函數,在main函數返回后執行
_fini函數。
★4 ELF的動態連接與裝載★4.1 動態連接當在UNIX系統下,用C編譯器把C源代碼編譯成可執行文件時,c編譯驅動器一般
將調用C的預處理,編譯器,匯編器和連接器。. c編譯驅動器首先把C源代碼傳到C的預處理器,它以處理過的宏和
指示器形式輸出純C語言代碼。. c編譯器把處理過的C語言代碼翻譯為機器相關的匯編代碼。. 匯編器把結果的匯編語言代碼翻譯成目標的機器指令。結果這些
機器指令就被存儲成指定的二進制文件格式,在這里,我們使用的
ELF格式。. 最后的階段,連接器連接所有的object文件,加入所有的啟動代碼和
在程序中引用的庫函數。 下面有兩種方法使用lib庫
--static library
一個集合,包含了那些object文件中包含的library例程和數據。用
該方法,連接時連接器將產生一個獨立的object文件(這些
object文件保存著程序所要引用的函數和數據)的copy。
--shared library
是共享文件,它包含了函數和數據。用這樣連接出來的程序僅在可執行
程序中存儲著共享庫的名字和一些程序引用到的標號。在運行時,動態
連接器(在ELF中也叫做程序解釋器)將把共享庫映象到進程的虛擬
地址空間里去,通過名字解析在共享庫中的標號。該處理過程也稱為
動態連接(dynamic linking)程序員不需要知道動態連接時用到的共享庫做什么,每件事情對程序員都是
透明的。
★4.2 動態裝載(Dynamic Loading)動態裝載是這樣一個過程:把共享庫放到執行時進程的地址空間,在庫中查找
函數的地址,然后調用那個函數,當不再需要的時候,卸載共享庫。它的執行
過程作為動態連接的服務接口。在ELF下,程序接口通常在中被定義。如下:void *dlopen(const char * filename,int flag);
const char * dlerror(void);
const void * dlsym (void handle*,const char * symbol);
int dlclose(void * handle);這些函數包含在libdl.so中。下面是個例子,展示動態裝載是如何工作的。
主程序在運行時動態的裝載共享庫。一方面可指出哪個共享庫被使用,哪個
函數被調用。一方面也能在訪問共享庫中的數據。[alert7@redhat62 dl]# cat dltest.c
#include
#include
#include
#include
#include typedef void (*func_t) (const char *);void dltest(const char *s)
{
printf("From dltest:");
for (;*s;s++)
{
putchar(toupper(*s));
}
putchar('\n');
}main(int argc,char **argv)
{
void *handle;
func_t fptr;
char * libname = "./libfoo.so";
char **name=NULL;
char *funcname = "foo";
char *param= "Dynamic Loading Test";
int ch;
int mode=RTLD_LAZY; while ((ch = getopt(argc,argv,"a:b:f:l:"))!=EOF)
{
switch(ch)
{
case 'a':/*argument*/
param=optarg;
break;
case 'b':/*how to bind*/
switch(*optarg)
{
case 'l':/*lazy*/
mode = RTLD_LAZY;
break;
case 'n':/*now*/
mode = RTLD_NOW;
break;
}
break;
case 'l':/*which shared library*/
libname= optarg;
break;
case 'f':/*which function*/
funcname= optarg;
}
}handle = dlopen(libname,mode);
if (handle ==NULL)
{
fprintf(stderr,"%s:dlopen:'%s'\n",libname,dlerror());
exit(1);
}fptr=(func_t)dlsym(handle,funcname);
if (fptr==NULL)
{
fprintf(stderr,"%s:dlsym:'%s'\n",funcname,dlerror());
exit(1);
}
name = (char **) dlsym(handle,"libname");
if (name==NULL)
{
fprintf(stderr,"%s:dlsym:'libname'\n",dlerror());
exit(1);
}printf("Call '%s' in '%s':\n",funcname,*name);/*call that function with 'param'*/
(*fptr)(param);dlclose(handle);
return 0;}這里有兩個共享庫,一個是libfoo.so一個是libbar.so。每個都用同樣的全局
字符串變量libname,分別各自有foo和bar函數。通過dlsym,對程序來說,他們
都是可用的。[alert7@redhat62 dl]# cat libbar.c
#include extern void dltest(const char *);
const char * const libname = "libbar.so";void bar (const char *s)
{
dltest("Called from libbar.");
printf("libbar:%s\n",s);
}
[alert7@redhat62 dl]# cat libfoo.c
#include extern void dltest (const char *s);
const char *const libname="libfoo.so";void foo(const char *s)
{
const char *saved=s;
dltest("Called from libfoo");
printf("libfoo:");
for (;*s;s++);
for (s--;s>=saved;s--)
{
putchar (*s);
}
putchar ('\n');
}使用Makefile文件來編譯共享庫和主程序是很有用的。因為libbar.so和
libfoo.so也調用了主程序里的dltest函數。[alert7@redhat62 dl] #cat Makefile
CC=gcc
LDFLAGS=-rdynamic
SHLDFLAGS=
RM=rmall:dltestlibfoo.o:libfoo.c
$(CC) -c -fPIC $
$(CC) $(SHLDFLAGS) -shared -o $@ $^libbar: libbar.c
$(CC) -c -fPIC $
$(CC) $(SHLDFLAGS) -shared -o $@ $^dltest: dltest.o libbar.so libfoo.so
$(CC) $(LDFLAGS) -o $@ dltest.o -ldlclean:
$(RM) *.o *.so dltest處理流程:[alert7@redhat62 dl]# export ELF_LD_LIBRARY_PATH=.
[alert7@redhat62 dl]# ./dltest
Call 'foo' in 'libfoo.so':
From dltest:CALLED FROM LIBFOO
libfoo:tseT gnidaoL cimanyD
[alert7@redhat62 dl]# ./dltest -f bar
bar:dlsym:'./libfoo.so: undefined symbol: bar'
[alert7@redhat62 dl]# ./dltest -f bar -l ./libbar.so
Call 'bar' in 'libbar.so':
From dltest:CALLED FROM LIBBAR.
libbar:Dynamic Loading Test
在動態裝載進程中調用的第一個函數就是dlopen,它使得共享可庫對
運行著的進程可用。dlopen返回一個handle,該handle被后面的dlsym
和dlclose函數使用。dlopen的參數為NULL有特殊的意思---它使得在
程序導出的標號和當前已經裝載進內存的共享庫導出的標號通過dlsym
就可利用。在一個共享庫已經裝載進運行著的進程的地址空間后,dlsym可用來
獲得在共享庫中導出的標號地址。然后就可以通過dlsym返回的地址
來訪問里面的函數和數據。當一個共享庫不再需要使用的時候,就可以調用dlclose卸載該函數庫。
假如共享庫在啟動時刻或者是通過其他的dlopen調用被裝載的話,該
共享庫不會從調用的進程的地址空間被移走。假如dlclose操作成功,返回為0。dlopen和dlsym如果有錯誤,將返回
為NULL。為了獲取診斷信息,可調用dlerror.
總結
以上是生活随笔為你收集整理的linux c 内存elf,关于LINUX下的可执行程序ELF(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何添加共享计算机用户,如何设置电脑联机
- 下一篇: TERRA-COTTA 之TCCONFI