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
- Linux-execve时的文件加载流程
- linux下execve实现的过程
- linux中execve函数的用法
- linux系统调用execve
- linux fork()和execve()
- linux下关于execve函数的简单解析
- 【小镇的技术之路】Linux中的execve函数
- execve()函数的研究
- execve的使用方法
- execve的使用方法
- execve的使用方法
- execve的用法举例
- execve的用法
- ecmall语言文件的加载使用流程
- linux /etc/profile,bashrc等文件的加载流程 以及 环境变量的设置
- execve
- linux开机流程 自定义模块的加载
- Linux系统网卡驱动的加载流程
- vue2 子组件调用父组件中data中的值
- iOS动态关联对象
- 算数移位(<<, >>)与逻辑移位
- 一篇写的很赞的HTTPS的文章
- echarts折线图实线与虚线拼接,及提示框浮层内容格式的设置
- Linux-execve时的文件加载流程
- jQuery 页面加载完成调用函数
- net use命令详解
- filter过滤器(java、servlet)
- Android 线程池
- WebStorm 2017.1.4激活码
- Java性能优化
- CC2538 zigbee I2C示例代码读写AT24C02D eeprom
- mysqli使用localhost问题 Warning: mysqli::mysqli(): (HY000/2002): No such file or directory in /...