Linux-execve时的文件加载流程

来源:互联网 发布:劫单挑亚索 知乎 编辑:程序博客网 时间:2024/05/19 03:30

这是n年前写的笔记,是以Arm Android环境为背景所写。今天看vDso时有些关联,就又翻出来瞧了瞧。


Elf文件的编译


在用户空间,用户代码可以编译为executable、static library,shared library、dynamic binaries。

  • Executable:根据编译脚本executable.mk,会自动将crtbegin_static.S和crtend.S链接进elf;elf的entry是crtbegin_static.S中的_start。
  • dynamic binaries: 根据编译脚本dynamic_binary.mk,会自动将crtbegin_dynamic.S和exidx_dynamic.c链接进elf;elf的entry是crtbegin_ dynamic.S中的_start。
  • shared library:根据编译脚本shared_library.mk,会自动将crtbegin_so.S和crtend_so.S链接进elf。
  • static library:编译脚本static_library.mk,编译成的static lib不能单独使用,需要被再链接进其他代码以便生成Executable/ dynamic binaries/ shared library。

编译脚本可参见目录build\core。


而连接器linker的编译,见bionic/linker/android.mk:

include $(BUILD_SYSTEM)/dynamic_binary.mk

可见将其按dynamic_binary方式编译了。


execve流程


当user运行一个exec或share lib的elf文件时,调用execve去执行一带X属性的文件。简单的流程如下:

user execve -> kernel load and link image -> kernel return to user                                                          -> _start-> __libc_init_common->main()

但有一种特殊情况,类似的文件加载和执行不是由user发起,而是由kernel主动发起。开机初始化阶段,在kernel已经初始化完毕,即将进入user space时,会主动加载user初始化文件”/init”,此行为和execve类似。


Kernel加载用户elf文件


内核初始化完毕后,会调用
run_init_process-> kernel_execve-> do_execve-> do_execve_common-> search_binary_handler-> load_elf_binary
去load文件“/init”并执行,此文件是EXEC类型。

如果是从用户空间通过系统调用请求execve,则从do_execve开始执行。

do_execve_common->bprm_mm_init:
依据当前线程,创建一个新的struct mm_struct *mm,其页表的kernel部分已经使用swap_page_dir初始化;创建一个4K的stack的struct vm_area_struct *vma,地址范围在即用户空间,如下:

(STACK_TOP_MAX-PAGE_SIZE) ~~~~ STACK_TOP_MAX.  

bprm->p指向vma->vm_end - sizeof(void *),即bprm->p为用户空间的sp。
说明: execve会覆盖进程原来的地址空间。

do_execve_common->prepare_binprm:
当检查发现文件被置为S_ISUID属性,即set-uid属性,则将文件所带的uid作为此进程的euid;如果文件带S_ISGID属性,则将文件的所带的gid作为进程的egid。默认情况下,进程的uid,gid都是继承自父进程。

load_elf_binary:

  • 查找programe segment有无PT_INTERP segment,如有,则加载的是此 interpret 文件,后期由interpret 文件去加载并执行目标可执行文件;否则直接打开可执行文件文件。
    在Ubuntu环境下,它的启动文件是”/sbin/init”,这是个共享库文件,是包含PT_INTERP segment的,其值是/lib/ld-linux.so.2,从而调用open_exec去打开的文件是“/lib/ld-linux.so.2”。
  • 对于PT_GNU_STACK类型的segment,如其属性带x,则表示可从stack中取指令。
  • 对于带PT_INTERP segment的情况,调用load_elf_interp去load文件“/lib/ld-linux.so.2”,返回所load的基地址和此文件的entry;如没有entry(比如share lib),则entry是其基地址。
  • 调用create_elf_tables,以便在env中添加环境变量
    AT_PHDR=load_addr + exec->e_phoff;
    AT_PHNUM= exec->e_phnum;
    AT_ENTRY = exec->e_entry
    AT_BASE =interp_load_addr : 将连接器的地址通过AT_BASE传递
    AT_SYSINFO_EHDR= current->mm->context.vdso,这就是vDso的code基地址
    。。。。。。
    注:对于在kernel所load的image和linker,会用elf_map将至映射至用户空间。所以此处的load_addr是用户空间地址。

  • 调用start_thread去填充stack,以便返回用户空间时能够跳转至“/lib/ld-linux.so.2”的entry,传入的参数为
    regs->ARM_r2 = stack[2]; /* r2 (envp) */
    regs->ARM_r1 = stack[1]; /* r1 (argv) */
    regs->ARM_r0 = stack[0]; /* r0 (argc) */

    即此时已经往用户栈中压了环境变量和参数。

  • 对于带PT_INTERP segment的情况,将interp文件的entry作为“/sbin/init”的enrty返回;

user space的elf流程


根据加载文件类型的不同,也有不同的初始化路径。 如果加载的是共享库,在user space的entry是linker的入口函数,在linker初始化后,由linker加载目标库函数。如果加载的是可执行文件,则user space的entry是其本身的入口函数。

linker:“/lib/ld-linux.so.2”

Linker被编译为static可执行文件,链接基地址是0xB0001000,链接脚本为\build\core\ armelf.x,可见其入口为ENTRY(_start)。_start定义在begin.S中,调用__linker_init后就去执行被链对象的入口函数。
linker从env中解析得到如下信息,从而可以跳转至/sbin/init的entry:

  AT_PHDR=load_addr + exec->e_phoff;  AT_PHNUM= exec->e_phnum;  AT_ENTRY = exec->e_entry

__linker_init -> debugger_init:
设置所在线程的异常sihnal处理函数。捕获SIGILL/SIGABRT/SIGBUS/SIGFPE/SIGSEGV/SIGSTKFLT/SIGPIPE,并用debugger_signal_handler处理之。

__linker_init-> link_image:
解析目标文件(“/sbin/init”),如果是exec文件,则load依赖的库文件ldpreload_names;如果dynamic seciton有属性为DT_NEEDED的section,则load对应的库文件。 对于“/sbin/init”,有如下依赖文件:

 0x00000001 (NEEDED)                     Shared library: [libnih.so.1] 0x00000001 (NEEDED)                     Shared library: [libnih-dbus.so.1] 0x00000001 (NEEDED)                     Shared library: [libdbus-1.so.3] 0x00000001 (NEEDED)                     Shared library: [libpthread.so.0] 0x00000001 (NEEDED)                     Shared library: [librt.so.1] 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

Load并定义好“/sbin/init”之后,linker跳转至“/sbin/init”的entry函数。


elf的初始化流程


库文件

加载linker的流程:
__linker_init-> link_image -> call_constructors->call_array(init_array)
->__libc_preinit
- ->__libc_init_common
- - ->__system_properties_init

如果是dynamic binary,其后则跳转至:
->_start(bionic/libc/arch-arm/bionic/crtbegin_dynamic.S)
->__libc_init(见文件bionic\libc\bionic\libc_init_dynamic.c)
->将__libc_fini添加至__atexit
->main()
->exit
->fini_array

如果是shared lib ,其后跳转至:
->_start (bionic/libc/arch-arm/bionic/crtbegin_so.S)

可执行文件

如果是static,其后则跳转至:
->_start(bionic/libc/arch-arm/bionic/crtbegin_static.S)
->__libc_init(见文件bionic\libc\bionic\libc_init_static.c)
- -> __libc_init_common
->preinit_array
->ctors_array
->init_array
->将__libc_fini添加至__atexit
->main()
->exit
->fini_array


__libc_init_common


由于execve是会覆盖原进程的地址空间,所以需要重新初始化user space,包括main thread,main thread TLS。(android bionic的pthread线程模型是在用户空间实现的)Main thread 在当前用户栈中默认分配了128K作为main thread的栈。TLS共有64个slot;

调用__system_properties_init为本进程初始化好系统属性访问环境,从而可以想属性服务发起属性访问。

调用__libc_init_vdso解析从AT_SYSINFO_EHDR传入的vdso。



以下是涉及到加载时的一些杂项,一并放这里了。


vDso


在加载elf文件时,kernel会调用 arch_setup_additional_pages 将vDso的code和参数区映射至user space,并将code的基地址保存在如下参数:

current->mm->context.vdso

所以进程共享同一个vdso所在的物理页面,但是每个进程的vdso地址是不一样的。vdso的地址是在user stack的地址之上,两者的间隔大小是随机产生的,可参见kernel的函数 vdso_addr(…)。

后期通过AT_SYSINFO_EHDR的方式将vdso的地址传递给user,user从而可以访问vDso。


dlopen


见文件dlfcn.c:
dlopen->find_library: 先从链表solist中查找此library是否已经loaded,如有,则返回;否则调用load_library去load此library,其后调用init_library去链接此library。

load_library:如是绝对路径,则调用_open_lib去打开此库文件;否则从路径ldpaths中查找此库,如成功,则打开;否则从路径sopaths中查找此库。
读取此库文件,并解析: 根据elf header,确认是否是合法的elf文件;如果是pre-link库文件,则从文件末尾处的prelink_info_t结构中得到所期望的映射地址mmap_addr;mmap一段空间用于load此库文件;调用load_segments将PT_LOAD的段读至RAM中;将相关的信息保存于soinfo结构中。

init_library->link_image: 如果是可执行文件,对于PT_LOAD段,记录下段的末尾地址,记录下不可些区域的范围,后期对此些区域做不可写保护处理;解析PT_DYNAMIC段,并记录下相关的信息;如果是可执行文件,则load数组ldpreload_names中所以需预先load的库,并loaded的库记录于数组preloads中;对于此文件本身需要依赖的库,被记录在elf的DT_NEEDED中,此时find_library此些依赖库;根据rel和plt_rel段,调用reloc_library做重定位工作;调用call_constructors去调用此库文件的初始化函数,包括preinit_array,init_func,init_array。


Debuger


Debugger采用的是client/service方式。
Services:

  • Debugger Service(system/core/debuggerd/debuggerd.c)创建local socket server
    “android:debuggerd”,以便监听其他线程的请求。
  • 收到请求后,调用handle_crashing_process处理debug请求。
    • 利用SO_PEERCRED,得到client进程的ucred,从而可得到pid;
    • 从socket读取tid;
    • 检查目录”/proc/pid/task/tid”是否存在,如不存在,则出错返回;
    • 在attach上目标线程之后,continue目标线程,让其异常,同时发送SIGSEGV等信号;
    • Debugger收到此些signal后,调用engrave_tombstone去dump线程现场;

Client:

  • 在linker中,在__linker_init中会调用debuggerd_init,即对于linker加载的线程,默认的debug是由debugger_signal_handler处理的。
  • debugger_signal_handler:
    • 创建clent socket “android:debuggerd”;
    • 将自己的tid通过socket写给service;
    • 从service读取debuggerd service的tid;

PT_ARM_EXIDX


此Program segment是用于unwind stack。
对于由linker加载的共享库,linker会在soinfo中记录下库所对应的PT_ARM_EXIDX的base和num。
同时linker也会export出函数dl_unwind_find_exidx,共查找指定地址的库所对应的soinfo,返回其对应的PT_ARM_EXIDX的base和num,供debuger去unwind stack。其中会对linker本身做特殊处理,如果指定的地址位于linker.so空间范围内,则返回出错。Linker的基地址和长度是在编译时指定的

LINKER_TEXT_BASE := 0xB0001000LINKER_AREA_SIZE := 0x01000000

对于static 或 executeable,则调用__gnu_Unwind_Find_exidx,此函数直接从链接脚本的__exidx_start、__exidx_end中提取出ARM_EXIDX数组。


android用户空间


关于prelink,见 bionic/linker/README.TXT。
以froyo编译出来的prelink-linux-arm.map为例,大概的分布如下:
XXXX



Done

原创粉丝点击