By Wing 程序启动的流程 在 linux 的环境中最常见的可执行档的种类包括了 Script 档、Aout 格式的执行档、ELF 格式的执行档。在本文的这个部分,我会针对 Linux 系统是如何来辨别这些不同的可执行档,以及整体的执行流程来作一个说明。 我在此大略说明一下程序启动的流程,当我们在 shell 中输入指令时,会先去系统的路径中来寻找是否有该可执行档存在,如果找不到的话,就会显示出找不到该可执行档的讯息。如果找到的话,就会去呼叫 execve()来执行该档案,接下来 execve() 会呼叫 System Call sys_execv(),这是在Linux 中 User Mode 透过 80 号中断(int 80 ah=11)进入 Kernel Mode 所执行的第一个指令,之後在 Kernel 中陆续执行 do_exec()、 prepare_binprm()、read_exec()、search_binary_handler(),而在 search_binary_handler() 函式中,会逐一的去检查目前所执行档案的型态(看看是否为Script File、aout 或 ELF 档),不过 Linux 所采用的方式是透过各个档案格式的处理程序来决定目前的执行档所属的处理程序。 如下图,会先去检验档案是否为 Script 档,若是直进入 Script 档的处理程序。若不是,则再进入 Aout 档案格式的处理程序,若该执行档为 Aout 的档案格式便交由 Aout档案格式的处理程序来执行。如果仍然不是的话,便再进入 ELF 档案格式的处理程序,如果都找不到的话,则传回错误讯息。 由这种执行的流程来看的话,如果 Linux Kernel 想要加入其他的执行档格式的话,就要在 search_binary_handler() 加入新的执行档的处理程序,这样一旦新的执行档格式产生後,在 Linux 下要执行时,因为在do_load_script、do_load_aout_binary、do_load_elf_binary都会传回错误,因此只有我们自己的 do_load_xxxx_binary 函式可以正确的接手整个执行档的处理流程,因此便可以达成新的档案格式置入的动作哩。 在函式 do_load_elf_binary () 执行时,首先会去检视目前的档案是否为 ELF 格式,如下程序码 便是去检查该档的前四个 bytes 是否为 0x7f 加上 “ELF” (0x 45 0x4c 0x46),若非,则结束 do_load_elf_binary 的执行。之後,便是去检视我们之前提过的 e_type 属性,来得知是否为 ET_EXEC(Executable File) 或是ET_DYN(Shared Object File) 这两个值的其中之一 如果都不是这两个值之一,便结束 do_load_elf_binary 的执行之後便是一连串读取 ELF 档表格的动作,在此就不多说,有兴趣的读者可以自行参阅/usr/src/linux/fs/binfmt_elf.c 的内容即可。 在此我们检视一个执行档由启动到结束的完整流程,首先这个执行档具有如下的程序码 然後,透过如下的编程过程 我们如果检视执行档的 ELF Header 可以得知它主要呼叫了 /lib/libc.so.6函式库中以下的函式 接下来,我们便把程序的执行流程大略整理如下,而 execve("./test", ["./test"], []) 执行的流程,就是刚刚我们所提到的内容,若不熟悉的读者,可以再回头看看刚刚的内容,即可对 execve("./test", ["./test"], []) 的执行流程有大略的了解。在这里,我们会把整个执行流程更完整的来检视一遍。 首先,我们所在的执行环境会透过 execve("./test", ["./test"], []) 的函式呼叫来启动 test 执行档。 呼叫 open("/etc/ld.so.cache", O_RDONLY),以唯读模式开启 ld.so.cache,这个档案的功能是作为动态函式库的快取,它会记录了目前系统中所存在的动态函式库的资讯以及这些函式库所存在的位置。所以说,如果我们在系统中安装了新的函式库时,我们便需要去更新这个档案的内容,以使新的函式库可以在我们的 Linux 环境中发生作用,我们可以透过 ldconfig 这个指令来更新 ld.so.cache 的内容。 呼叫 mmap(0, 9937, PROT_READ, MAP_PRIVATE, 3, 0),把 ld.so.cache 档案映射到记忆体中,mmap 函式的宣告为 mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset),在笔者的电脑上 ld.so.cache 的档案大小为 9937 bytes,PROT_READ代表这块记忆体位置是可读取的,MAP_PRIVATE 则表示产生一个行程私有的 copy-on-write 映射,因此这个呼叫会把整个 ld.so.cache 档案映射到记忆体中,在笔者电脑上所传回的映射记忆体起始位置为 0x40013000。 注: mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)代表我们要求在档案 fd中,起始位置为offset去映射 length 长度的资料,到记忆体位置 start ,而 prot 是用来描述该记忆体位置的保护权限(例如:读、写、执行),flags用来定义所映射物件的型态,例如这块记忆体是否允许多个 Process 同时映射到,也就是说一旦有一个 Process 更改了这个记忆体空间,那所有映射到这块记忆体的Process 都会受到影响,或是 flag 设定为 Process 私有的记忆体映射,这样就会透过 copy-on-write 的机制,当这块记忆体被别的 Process 修改後,会自动配置实体的记忆体位置,让其他的 Process 所映射到的记忆体内容与原本的相同。(有关mmap的其它应用,可参考本文最後的注一) 呼叫 open("/lib/libc.so.6", O_RDONLY),开启 libc.so.6。 呼叫 read(3, "177ELF111331250202"..., 4096) 读取libc.so.6的档头。 呼叫 mmap(0, 993500, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0),把 libc.so.6 映射到记忆体中,由档头开始映射 993500 bytes,若是使用 RedHat 6.1(或其它版本的 RedHat)的读者或许会好奇 libc.so.6 所 link 到的档案 libc-2.1.2.so 大小不是 4118715 bytes 吗? 其实原本 RedHat 所附的 libc.so.6 动态函式库是没有经过 strip 过的,如果经过 strip 後,大小会变为 1052428 bytes,而 libc.so.6 由档头开始在 993500 bytes 之後都是一些版本的资讯,笔者猜想应该是这样的原因,所以在映射档时,并没有把整个 libc.so.6 档案映射到记忆体中,只映射前面有意义的部分。与映射 ld.so.cache 不同的是,除了 PROT_READ 属性之外,libc.so.6 的属性还多了PROT_EXEC,这代表了所映射的这块记忆体是可读可执行的。在笔者的电脑中,libc.so.6 所映射到的记忆体起始位置为 0x40016000。 呼叫 mprotect(0x40101000, 30940, PROT_NONE),用来设定记忆体的使用权限,而 PROT_NONE 属性是代表这块记忆体区间(0x40101000—0x401088DC)是不能读取、写入与执行的。 呼叫 mmap(0x40101000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xea000),映射 libc.so.6 由起始位置 0xea000 映射 16384bytes 到记忆体位置 0x40101000。 呼叫 mmap(0x40105000, 14556, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0),MAP_ANONYMOUS 表示没有档案被映射,且产生一个初始值全为 0 的记忆体区块。 呼叫 munmap(0x40013000, 9937),把原本映射到 ld.so.cache 的记忆体解除映射(此时已把执行档所需的动态函式库都映射到记忆体中了)。 呼叫 personality(0),可以设定目前 Process 的执行区间(execution domain),换个说法就是 Linux 支援了多个执行区间,而我们所设定的执行区间会告诉 Linux 如何去映射我们的讯息号码(signal numbers)到各个不同的讯息动作(signal actions)中。这执行区间的功能,允许 Linux 对其它 Unix-Like 的操作系统,提供有限度的二进位档支援。如这个例子中,personality(0) 的参数为 0,就是指定为 PER_LINUX 的执行区间(execution domain)。 呼叫 getpid(),取得目前 Process 的 Process ID。 呼叫 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0),传回值为 0x400130,MAP_ANONYMOUS 表示没有档案被映射,且产生一个初始值全为 0 的记忆体区块。 呼叫 write(1, " test ", 6),显示字串在画面上。 呼叫 munmap(0x40013000, 4096),解除记忆体位置0x40013000的记忆体映射。 呼叫 _exit(6),结束程序执行。 在这段所举的例子,只用到了一个函式库 libc.so.6,我们可以举像是 RedHat 中 Telnet 指令为例,首先检视他的 ELF Header 它主要呼叫了函式库 libncurses.so.4 的函式 tgetent,以及函式库 libc.so.6 中为数不少的函式,当然我们也可以去检视它执行的流程,与之前只呼叫了 libc.so.6 的printf 函式来比较,我们可以发现它主要的不同就是去载入了 libncurses.so.4 clearcase/" target="_blank" >cccccc">
if (elf_ex.e_ident[0] != 0x7f' '
strncmp(&elf_ex.e_ident[1], "ELF", 3) != 0)
goto out;
if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN)
goto out;
#include
int main()
{
printf(" test ");
}
gcc test.c ˉo test
printf
__deregister_frame_info
__libc_start_main
__register_frame_info
#define PER_MASK (0x00ff)
#define PER_LINUX (0x0000)
#define PER_LINUX_32BIT (0x0000 | ADDR_LIMIT_32BIT)
#define PER_SVR4 (0x0001 | STICKY_TIMEOUTS)
#define PER_SVR3 (0x0002 | STICKY_TIMEOUTS)
#define PER_SCOSVR3 (0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS)
#define PER_WYSEV386 (0x0004 | STICKY_TIMEOUTS)
#define PER_ISCR4 (0x0005 | STICKY_TIMEOUTS)
#define PER_BSD (0x0006)
#define PER_XENIX (0x0007 | STICKY_TIMEOUTS)
#define PER_LINUX32 (0x0008)
#define PER_IRIX32 (0x0009 | STICKY_TIMEOUTS) /* IRIX5 32-bit */
#define PER_IRIXN32 (0x000a | STICKY_TIMEOUTS) /* IRIX6 new 32-bit */
#define PER_IRIX64 (0x000b | STICKY_TIMEOUTS) /* IRIX6 64-bit */
==>libncurses.so.4
tgetent
==>libc.so.6
strcpy
ioctl
printf
cfgetospeed
recv
connect
............┅
sigsetmask
__register_frame_info
close
free
open("/usr/lib/libncurses.so.4", O_RDONLY) ;
fstat(3, {st_mode=S_IFREG|0755, st_size=274985, ...}) ;
read(3, "177ELF111331340335"..., 4096) ;
mmap(0, 254540, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0);
mprotect(0x40048000, 49740, PROT_NONE);
mmap(0x40048000, 36864, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_FIXED, 3, 0x31000);
mmap(0x40051000, 12876, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) ;
close(3);