从程序到进程

该文出自:http://www.civilnet.cn/bbs/browse.php?topicno=78426

 

本文以《从代码到可执行文件》为基础,阅读本文前确保你熟悉了《从代码到可执行文件》中提到的概念,本文中的示例程序仍是《从代码到可执行文件》中的gemfield.c。代码如下:

**************************** int main(int argc,char *argv[]) { int a = 0; char gemfield[32]; printf(“input gemfield’s blog: “); scanf(“%s”,gemfield); printf(“gemfield’s blog is %s\n”,gemfield); } ****************************

第一步:编译。 gcc gemfield.c -o gemfield

第二步:运行。

./gemfield &

输出:[1] 19649 gemfield@gemfield:~$ input gemfield’s blog:

第三步:ps命令 ps -e|grep gemfield

输出: 19649 00:00:00 gemfield

表明进程id为19649的gemfield进程已经产生了。

第四步:查看进程gemfield的cmdline 切换到内核映像 proc目录下: cd /proc/19649 cat cmdline

输出: ./gemfield

正是程序运行的参数

第五步:查看进程gemfield的环境参数 切换到内核映像 proc目录下: cd /proc/19649 cat environ 输出: http://civilnet.cn/bbs/topicno/71165

第六步:查看进程gemfield所使用的文件 切换到内核映像 proc目录下: cd /proc/19649 ls fd 输出: 0 1 2 说明目前gemfield进程只使用了标准输入、输出、错误。

第七步、查看进程gemfield所使用的io 切换到内核映像 proc目录下: cd /proc/19649 cat io 请访问http://civilnet.cn/bbs/topicno/71162 来查看输出的含义

第八步:查看进程gemfield的内存映射 切换到内核映像 proc目录下: cd /proc/19649 cat maps 输出: 08048000-08049000 r-xp 00000000 fc:00 12845748 /home/gemfield/gemfield 08049000-0804a000 r–p 00000000 fc:00 12845748 /home/gemfield/gemfield 0804a000-0804b000 rw-p 00001000 fc:00 12845748 /home/gemfield/gemfield b75c0000-b75c1000 rw-p 00000000 00:00 0  b75c1000-b7737000 r-xp 00000000 fc:00 10747923 /lib/i386-linux-gnu/libc-2.13.so b7737000-b7739000 r–p 00176000 fc:00 10747923 /lib/i386-linux-gnu/libc-2.13.so b7739000-b773a000 rw-p 00178000 fc:00 10747923 /lib/i386-linux-gnu/libc-2.13.so b773a000-b773d000 rw-p 00000000 00:00 0  b7741000-b7745000 rw-p 00000000 00:00 0  b7745000-b7746000 r-xp 00000000 00:00 0 [vdso] b7746000-b7764000 r-xp 00000000 fc:00 10747920 /lib/i386-linux-gnu/ld-2.13.so b7764000-b7765000 r–p 0001d000 fc:00 10747920 /lib/i386-linux-gnu/ld-2.13.so b7765000-b7766000 rw-p 0001e000 fc:00 10747920 /lib/i386-linux-gnu/ld-2.13.so bfd61000-bfd82000 rw-p 00000000 00:00 0 [stack] cat smaps 输出: http://civilnet.cn/bbs/topicno/71166

第九步:查看进程gemfield的状态 切换到内核映像 proc目录下: cd /proc/19649 cat status 输出: Name: gemfield State: T (stopped) Tgid: 19649 Pid: 19649 PPid: 19548 TracerPid: 0 Uid: 1000 1000 1000 1000 Gid: 1000 1000 1000 1000 FDSize: 256 Groups: 4 20 24 46 112 113 114 1000  VmPeak: 1836 kB VmSize: 1820 kB VmLck: 0 kB VmHWM: 248 kB VmRSS: 248 kB VmData: 36 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1616 kB VmPTE: 16 kB VmSwap: 0 kB Threads: 1 SigQ: 0/31392 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: 0000000000000000 SigCgt: 0000000000000000 CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: ffffffffffffffff Cpus_allowed: f Cpus_allowed_list: 0-3 Mems_allowed: 1 Mems_allowed_list: 0 voluntary_ctxt_switches: 1 nonvoluntary_ctxt_switches: 3

第十步、查看进程gemfield的调度信息 切换到内核映像 proc目录下: cd /proc/19649 cat sched 当查看这个文件时, 定义在kernel/sched_debug.c中的proc_sched_show_task() 函数将会被调用。 输出: gemfield (19649, #threads: 1) ——————————————————— se.exec_start : 2573022261.330409 se.vruntime : 271.916852 se.sum_exec_runtime : 0.569372 se.statistics.wait_start : 0.000000 se.statistics.sleep_start : 0.000000 se.statistics.block_start : 0.000000 se.statistics.sleep_max : 0.000000 se.statistics.block_max : 0.000000 se.statistics.exec_max : 0.384138 se.statistics.slice_max : 0.000000 se.statistics.wait_max : 0.024456 se.statistics.wait_sum : 0.030986 se.statistics.wait_count : 5 se.statistics.iowait_sum : 0.000000 se.statistics.iowait_count : 0 se.nr_migrations : 2 se.statistics.nr_migrations_cold : 0 se.statistics.nr_failed_migrations_affine: 0 se.statistics.nr_failed_migrations_running: 0 se.statistics.nr_failed_migrations_hot: 0 se.statistics.nr_forced_migrations : 0 se.statistics.nr_wakeups : 1 se.statistics.nr_wakeups_sync : 0 se.statistics.nr_wakeups_migrate : 0 se.statistics.nr_wakeups_local : 1 se.statistics.nr_wakeups_remote : 0 se.statistics.nr_wakeups_affine : 0 se.statistics.nr_wakeups_affine_attempts: 0 se.statistics.nr_wakeups_passive : 0 se.statistics.nr_wakeups_idle : 0 avg_atom : 0.142343 avg_per_cpu : 0.284686 nr_switches : 4 nr_voluntary_switches : 1 nr_involuntary_switches : 3 se.load.weight : 1024 policy : 0 prio : 120 clock-delta : 49

第十一步、粗略描述 我们还可以从 /proc/mount* 查看进程gemifeld使用的分区信息 /proc/net/* 查看进程gemfield使用的网络设备情况

步骤先放一放,那究竟什么是进程呢?

进程就是程序在计算机系统上运行时的一个实例,管理着计算机系统分配给它的各种资源,如:

1、程序的可运行机器码的一个在内存中的映像; 2、分配到的内存,包括可运行代码、特定于进程的数据(输入、输出)、堆、栈(用于保存运行时运输中途产生的数据); 3、分配给该进程的资源的操作系统描述子,诸如文件描述子(Unix 术语)或文件句柄(Windows)、数据源和数据终端; 4、安全特性,诸如进程拥有者和进程的权限集(可以容许的操作); 5、处理器状态(上下文),如寄存器内容等。当进程正在运行时,状态通常存储在cpu的寄存器,其他情况则在内存中。

先在清除为什么本文要花费大片费来介绍/proc/pid下面的内容了吧,在建立了一个直观的印象后,我们深入到Linux的内核中来看看进程,在内核中,进程是由一个叫作task_struct的结构体 来维护的。

第十二步,分析task_struct结构体。

让我们先行猜测下这个结构体应该有什么内容吧,从前十一步得知,它应该有下面的内容: 1、进程的id什么的; 2、进程的状态,在等待,运行,或死锁; 3、进程的家族,像吸血鬼家族一样,有吸血鬼祖先、后代、还有僵尸; 4、进程的内存映射; 5、时间片信息,以便cpu调度; 6、使用的文件描述符; 7、处理器寄存器的上下文。

那我们来实际看看这个结构体,在/include/linux/sched.h中有task_struct的定义: *************************************************************************** struct task_struct { volatile long state;//运行时状态,-1不可运行,0可运行,>0已停止  unsigned long flags; //flage是当前的进程标志,有正在被创建、正准备退出、被fork出但是没有执行exec、由于其他进程发送相关信号而被杀死  int sigpending; //进程上是否有待处理的信号 mm_segment_t addr_limit;  volatile long need_resched;//调度标志,表示该进程是否需要重新调度. int lock_depth; //锁深度 long nice; //进程的基本时间片 unsigned long policy;//调度策略有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER struct mm_struct *mm; //进程内存信息 int processor; unsigned long cpus_runnable, cpus_allowed;//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新 struct list_head run_list; //指向运行队列的指针 unsigned long sleep_time; //进程的睡眠时间 struct task_struct *next_task, *prev_task;//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task struct mm_struct *active_mm; struct list_head local_pages;//指向本地页面  unsigned int allocation_order, nr_local_pages; struct linux_binfmt *binfmt;//进程所运行的可执行文件的格式 int exit_code, exit_signal; int pdeath_signal; //父进程终止是向子进程发送的信号 unsigned long personality; int did_exec:1;  pid_t pid; //进程号 pid_t pgrp; //进程组标识,表示进程所属的进程组 pid_t tty_old_pgrp; //进程控制终端所在的组标识 pid_t session; //进程的会话标识 pid_t tgid;//进程组号 int leader; //表示进程是否为会话主管 struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr; struct list_head thread_group; //线程链表 struct task_struct *pidhash_next; //用于将进程链入HASH表 struct task_struct **pidhash_pprev; wait_queue_head_t wait_chldexit; //供wait4()使用 struct completion *vfork_done; //供vfork() 使用 unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值 unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_value; struct timer_list real_timer; //指向实时定时器的指针 struct tms times; //记录进程消耗的时间 unsigned long start_time; //进程创建的时间 long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];  unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;//参考下面 int swappable:1; //表示进程的虚拟地址空间是否允许换出 uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; //记录进程在多少个用户组中 gid_t groups[NGROUPS]; //记录进程所在的组 kernel_cap_t cap_effective, cap_inheritable, cap_permitted; int keep_capabilities:1; struct user_struct *user; struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息 unsigned short used_math; //是否使用FPU char comm[16]; //进程正在运行的可执行文件名 int link_count, total_link_count; struct tty_struct *tty;//NULL if no tty  unsigned int locks; struct sem_undo *semundo; //进程在信号灯上的所有undo操作 struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作 struct thread_struct thread;//存放cpu寄存器的上下文,参考下面。 struct fs_struct *fs; //文件系统信息 struct files_struct *files;//打开文件信息 spinlock_t sigmask_lock; //信号处理函数 struct signal_struct *sig; //信号处理函数 sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位 struct sigpending pending; //进程上是否有待处理的信号 unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock; void *journal_info; }; 内存缺页和交换信息:min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中。

struct thread_struct { struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES]; TLS? unsigned long esp0; unsigned long sysenter_cs; unsigned long eip; unsigned long esp; unsigned long fs; unsigned long gs;

unsigned long debugreg[8]; 调试相关的寄存器内容 unsigned long cr2, trap_no, error_code; union i387_union i387; 保存数学协处理器相关寄存器的内容 struct vm86_struct __user * vm86_info; unsigned long screen_bitmap; unsigned long v86flags, v86mask, saved_esp0; unsigned int saved_fs, saved_gs; unsigned long *io_bitmap_ptr; 保存当前进程的I/O权限位图 unsigned long io_bitmap_max; }; *********************************************************************************

第十三步、从ELF启航——fork

话说gemfield.c被编译为gemfield之后,./gemfield 是怎样将gemfield这个ELF文件装入到内存中的呢?

1、首先,不管你是在程序中产生子进程,还是在shell中启动程序,都会使用fork系统调用. 一个有趣的例子参考这里:http://civilnet.cn/bbs/topicno/71135 (代码最少的bash)

2、fork函数调用的过程大致如下: 程序调用fork()–>库函数fork()–>系统调用(fork功能号)–>由功能号在 sys_call_table[]中寻到sys_fork()函数地址–>调用sys_fork()–〉do_fork(),这就完成拉从用户态到内核态的 变化过程。

3、do_fork的实现:

p = copy_process(clone_flags, stack_start, regs, stack_size,child_tidptr, NULL, trace); wake_up_new_task(p, clone_flags);

第一步是调用copy_process函数来复制一个进程,并对相应的标志位等进行设置,接下来,如果copy_process调用成功的话,那么系统会有意让新开辟的进程运行,这是因为子进程一般都会 马上调用exec()函数来执行其他的任务。

4、copy_process的实现: ************************************************************************************ 1、p = dup_task_struct(current); 2、为新进程创建一个内核栈、thread_iofo和task_struct,这里完全copy父进程的内容(相当于把上文中的的task_struct内容完整的拷贝了一份),到目前为止,父进程和子进程是没有任何区别的。 3、检查所有的进程数目是否已经超出了系统规定的最大进程数,如果没有的话,那么就开始设置进程描述符中的初始值,从这开始,父进程和子进程就开始区别开了。 4、设置子进程的状态为不可被TASK_UNINTERRUPTIBLE,从而保证这个进程现在不能被投入运行,因为还有很多的标志位、数据等没有被设置。 5、复制标志位(falgs成员)以及权限位(PE_SUPERPRIV)和其他的一些标志。 6、调用get_pid()给子进程获取一个有效的并且是唯一的进程标识符PID。 7、根据传入的cloning flags(参考:http://civilnet.cn/bbs/topicno/71163)对相应的内容进行copy。比如说打开的文件符号、信号等。 8、父子进程平分父进程剩余的时间片。 9、return p;返回一个指向子进程的指针。 ************************************************************************************

第十四步:exec()函数家族

在第十三步中的fork虽然在系统中产生了一个新的进程,但基本没什么用;因为新的进程的逻辑和数据还在gemfield这个二进制文件中,所以,这里肯定是怎么把gemfield这个ELF文件填充到新的task_struct中了。这就是exec()家族的作用了。

1、exec家族的各个函数的调用最终会调用c函数库中的 execve(),这个函数的原型如下: int execve(const char * filename,char * const argv[],char * const envp[]); 可以看出来其接受3个参数,分别是:程序文件名、程序参数、程序环境变量,这个在第一~十三步中都有提及;同时,第一个参数为程序文件名的事实告诉我们,gemfield可执行文件将要依靠这个函数被装入了; 2、execve()使用系统调用sys_execve(),后者进行参数检查后,再使用do_execve()系统调用; 3、do_execve()根据传过来的参数寻找gemfield程序。

第十五步:do_execve()系统调用

1、开辟一个linux_binprm结构体(在/usr/src/linux/include/linux/binfmts.h中),根据gemfield这个二进制文件来进行填充; ********************************************************* struct linux_binprm{ char buf[BINPRM_BUF_SIZE]; struct page * page[MAX_ARG_PAGES];//#ifdef __KERNEL__#define MAX_ARG_PAGES 32 struct mm_struct * mm; unsigned long p; //current top of mem int sh_bang; struct file * file; int e_uid, e_gid; kernel_cap_t cap_inheritable, cap_permitted, cap_effective; void * security; int argc, envc; char * filename; //Name of binary as seen by procps  char * interp; //Name of the binary really executed unsigned interp_flags; unsigned interp_data; unsigned long loader, exec; }; ******************************************************** 2、调用path_lookup(), dentry_open(), and path_release() 来获得和gemfield文件相关的dentry object、file object、inode object,关于这三者,参考: http://civilnet.cn/bbs/topicno/71164

3、通过查看inode结构的i_writecount域来检查gemfield没有被正在写入,然后在i_writecount存入-1来防止其他写入;

4、在多cpu系统中,通过sched_exec()来判断使用哪个cpu来执行gemfield;

5、调用init_new_context() 来判断当前进程是否在使用自定义的Local Descriptor Table,如果是的话, 这个函数为gemfield分配并填充一个新的LDT;

6、调用prepare_binprm() 函数来填充linux_binprm 数据结构:

*检查gemfield是否是可执行的; *初始化linux_binprm 结构的e_uid 和 e_gid 域 ; *使用gemfield的前128个字节填充linux_binprm结构的buf域 . 这128个字节包含了魔数和其他辨别可执行文件身份的信息(参考:从代码到可执行文件)。

7、拷贝gemfield文件路径名、命令行参数、环境参数到一个或多个新分配的page frames. (它们将最终被赋值给用户空间);

8、调用search_binary_handler() 函数, 这个函数扫描可执行格式的链表,来判断是否有适用于gemfield这种ELF格式的load_binary函数,如果找到的话,就将linux_binprm这个结构体传给 load_binary函数,最后再释放linux_binprm 数据结构;

第十六步、load_binary的工作 load_binary 函数的实施按照以下步骤:

1、判断gemfield的魔数是否匹配;

2、通过kernel_read()读取gemfield的ELF header,ELF的头包含了程序的段和共享库信息,代码如下: ********************************************* size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr); retval = -ENOMEM; elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL); if (!elf_phdata) goto out; retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *) elf_phdata, size); …… …… files = current->files;   …… …… retval = get_unused_fd(); …… …… get_file(bprm->file); fd_install(elf_exec_fileno = retval, bprm->file); elf_ppnt = elf_phdata; elf_bss = 0; elf_brk = 0; start_code = ~0UL; end_code = 0; start_data = 0; end_data = 0; ******************************************** 还为已打开的gemfield映像文件在当前进程的打开文件表中另外分配一个表项,类似于执行了一次dup(),目的在于为gemfield维持两个不同的上下文,以便从不同的位置上读出;

接着是对elf_bss 、elf_brk、start_code、end_code等等变量的初始化。这些变量分别纪录着当前(到此刻为止)目标映像的bss段、代码段、数据段、以及动态分配“堆” 在用户空间的位置 。除start_code的初始值为0xffffffff外,其余均为0。随着映像内容的装入,这些变量也会逐步得到调整。 ******************************************** 3、获取dynamic linker的路径名(比如/lib/ld-linux.so.2),dynamic linker将把共享库映射到内存中;ELF格式的二进制映像在装入和启动的过程中需要得到一个工具软件的协助,其主要的目的在于为目标映像建立起跟共享库的动态连接。这个工具称为“dynamic linker”。一个ELF映像在装入时需要用什么dynamic linker是在编译/连接是就决定好了的,这信息就保存在映像的“解释器”部中。“解释器”部的类型为PT_INTERP,找到后就根据其位置p_offset和大小p_filesz把整个“dynamic linker”部读入缓冲区。整个“解释器”部实际上只是一个字符串,即解释器的文件名,例如“/lib/ld-linux.so.2”。有了解释器的文件名以后,就通过open_exec()打开这个文件,再通过kernel_read()读入其开头128个字节,这就是映像的头部。早期的dynamic linker映像是a.out格式的,现在已经都是ELF格式的了,/lib/ld-linux.so.2就是个ELF映像。 4、获取dynamic linker的dentry object 、inode object、file object;

5、检查dynamic linker的执行权限;

6、将dynamic linker的前128个字节拷贝到一个buffer中;

7、实施一些dynamic linker类型的一致性检查;

8、至此,我们已为目标映像和dynamic linker映像的装入作好了准备。可以让当前进程(线程)与其父进程分道扬镳,转化成真正意义上的进程,走自己的路了: ************************************************** retval = flush_old_exec(bprm); …… /* OK, This is the point of no return */ current->mm->start_data = 0; current->mm->end_data = 0; current->mm->end_code = 0; current->mm->mmap = NULL; current->flags &= ~PF_FORKNOEXEC; current->mm->def_flags = def_flags; …… retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); ***************************************************

调用flush_old_exec() 函数释放之前使用的资源,并且通过flush_old_exec()函数实施以下步骤: **************************************************** a、如果信号处理表是和其他进程共享的, 这个函数就分配一个新表并将旧的进程的相关计数器减一;并且,它从旧的进程组中分离 ,所有这些是通过调用de_thread()函数完成的;

b、调用unshare_files() 来复制一份files_struct 结构,里面包含了进程打开的文件的信息;

c、调用exec_mmap() 函数释放掉内存描述符、所有的内存区域、所有分配给gemfield进程的page frames ,并且将进程的页表清空;

d、将gemfield进程描述符中的comm域设置为gemfield可执行文件的路径名;

e、调用flush_thread() 函数保存在TSS段中的浮点寄存器和调试寄存器的值清空;

f、通过调用flush_signal_handlers()函数来重新设置信号处理表为默认值;

g、调用flush_old_files() 函数,将“程描述符中的files->close_on_exec域使能”的文件全部关掉;

*****************************************************

通过上面的步骤,flush_old_exec()把当前进程用户空间的页面都释放了。这么一来,当前进程的用户空间是全新的。接下来要重建用户空间的映射了。一个新的映像要能运行,用户空间堆栈是必须的,所以首先要把用户空间的一个虚拟地址区间划出来用于堆栈。进一步,当CPU进入新映像的程序入口时,堆栈上应该有argc、argv[]、envc、envp[]等参数。这些参数来自老的程序,需要通过堆栈把它们传递给新的映像。实际上,argv[]和envp[]中是一些字符串指针,光把指针传给新映像,而不把相应的字符串传递给新映像,那是毫无意义的。为此,在进入search_binary_handler()、从而进入load_elf_binary()之前,do_execve()已经为这些字符串分配了若干页面,并通过copy_strings()从用户空间把这些字符串拷贝到了这些页面中。现在则要把这些页面再映射回用户空间(当然是在不同的地址上),这就是这里setup_arg_pages()要做的事。这些页面映射的地址是在用户空间堆栈的最顶部。对于x86处理器,用户空间堆栈是从3GB 边界开始向下伸展的,首先就是存放着这些字符串的页面,再往下才是真正意义上的用户空间堆栈。而argc、argv[]这些参数,则就在这真正意义上的用户空间堆栈上。

下面就可以装入新映像了。所谓“装入”,实际上就是将映像的(部分)内容映射到用户(虚拟地址)空间的某些区间中去。在MMU的swap机制的作用下,这个过程甚至并不需要真的把映像的内容读入物理页面,而把实际的读入留待将来的缺页中断。 ************************************************************************ for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { int elf_prot = 0, elf_flags; unsigned long k, vaddr;

if (elf_ppnt->p_type != PT_LOAD) continue; ……

vaddr = elf_ppnt->p_vaddr; if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) { elf_flags |= MAP_FIXED; } else if (loc->elf_ex.e_type == ET_DYN) { /* Try and get dynamic programs out of the way of the default mmap base, as well as whatever program they might try to exec.  This is because the brk will follow the loader, and is not movable.  */ load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr); }

error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags); ……

if (!load_addr_set) { load_addr_set = 1; load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset); if (loc->elf_ex.e_type == ET_DYN) { load_bias += error - ELF_PAGESTART(load_bias + vaddr); load_addr += load_bias; reloc_func_desc = load_bias; } } k = elf_ppnt->p_vaddr; if (k < start_code) start_code = k; if (start_data < k) start_data = k; …… k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;

if (k > elf_bss) elf_bss = k; if ((elf_ppnt->p_flags & PF_X) && end_code < k) end_code = k; if (end_data < k) end_data = k; k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; if (k > elf_brk) elf_brk = k; }                                                      //end for() loop loc->elf_ex.e_entry += load_bias; elf_bss += load_bias; elf_brk += load_bias; start_code += load_bias; end_code += load_bias; start_data += load_bias; end_data += load_bias;

/* Calling set_brk effectively mmaps the pages that we need * for the bss and break sections.  We must do this before * mapping in the interpreter, to make sure it doesn’t wind * up getting placed where the bss needs to go. */ retval = set_brk(elf_bss, elf_brk); …… ***************************************************************************

9、将gemfield进程描述符中的PF_FORKNOEXEC标志清零,这个标志在gemfield进程被forked的时候置位,而在执行新的代码后被清零;

10、设置进程描述符中personality域以新的值;

11、调用arch_pick_mmap_layout() 来选择gemfield进程的内存区域布局;

12、调用setup_arg_pages() 函数为gemfield进程的用空空间栈分配一个新的memory region描述符,并将这个memory region 插入到gemfield进程的地址空间;setup_arg_pages() 同时将含有命令行参数和环境参数的page frames分配到这个新的memory region上;

13、调用do_mmap()函数来创建一个新的memory region,并映射到gemfield可执行文件的代码段,这个memory region的初始线形地址取决于可执行文件的格式,因为程序的代码段通常是不能重定位的,因此,do_mmap()认为代码段是从特定的地址开始的,ELF格式的(就像gemfield)是从线形地址0×08048000处开始的;

14、调用do_mmap()函数来创建一个新的memory region,并映射到gemfield可执行文件的数据段,这个memory region的初始线形地址取决于可执行文件的格式,因为可执行代码期望从特定的偏移处找到所需的变量,ELF格式的(就像gemfield)是数据段是刚好在代码段之后装入的;

15、为gemfield可执行文件的其他可装入段分配额外的memory regions ;

16、调用load_elf_interp()函数装入dynamic linker,通常这个步骤和12~14步是类似的, 为了防止和gemfield这种可执行文件在内存上的冲突,dynamic linker的初始地址是在 0×40000000之上,代码如下 : ********************************************** if (elf_interpreter) {   if (interpreter_type == INTERPRETER_AOUT)  elf_entry = load_aout_interp(&loc->interp_ex, interpreter);   else  elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_load_addr);   ……   reloc_func_desc = interp_load_addr;

  allow_write_access(interpreter);   fput(interpreter);   kfree(elf_interpreter); } else {   elf_entry = loc->elf_ex.e_entry; } ************************************************** 如果需要装入dynamic linker,并且dynamic linker的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是dynamic linker的程序入口;而若不装入dynamic linker,那么这个地址就是目标映像本身的程序入口。 17、Stores in the binfmt field of the process descriptor the address of the linux_binfmt object of the executable format.

18、决定gemfield进程的新capabilities;

19、创建特定的动态链接表并放在用户态栈的命令行参数和环境字符串指针数组的中间(参考下图);

20、设置gemfield进程的内存描述符中的start_code, end_code, start_data, end_data, start_brk, brk, start_stack 这些域的值; ****************************************** struct mm_struct { struct vm_area_struct * mmap;//指向虚拟区间(VMA)链表  rb_root_t mm_rb; //指向red_black树 struct vm_area_struct * mmap_cache; //指向最近找到的虚拟区间 pgd_t * pgd; //指向进程的页目录 atomic_t mm_users;//用户空间中的有多少用户  atomic_t mm_count; //对”struct mm_struct”有多少引用  int map_count; //虚拟区间的个数 struct rw_semaphore mmap_sem; spinlock_t page_table_lock;//保护任务页表和 mm->rss  struct list_head mmlist; //所有活动(active)mm的链表  unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; unsigned long rss, total_vm, locked_vm; unsigned long def_flags; unsigned long cpu_vm_mask; unsigned long swap_address; unsigned dumpable:1; /* Architecture-specific MM context */ mm_context_t context; }; ************************************************** 21、调用do_brk()函数来创建一个新的匿名的memory region,并映射到gemfield文件的bss段,当gemfield写一个变量时, 将触发缺页中断,从而导致一个page frame的分配; 这个新的memory region的大小是在程序链接的时候计算好的,并且初始的线形地址必须是指定的,在ELF格式的程序中(如gemfield), bss 装载在数据段之后;

22、调用start_thread() 宏来设置用户态寄存器eip和esp的值,这些值保存在内核态堆栈上,以使它们分别指向dynamic linker的入口点和新的用户态栈的顶点;start_thread()是个宏操作 ,其定义如下: ************************************************** #define start_thread(regs, new_eip, new_esp) do {  \        __asm__(“movl %0,%%fs ; movl %0,%%gs”: :”r” (0));  \        set_fs(USER_DS);                   \        regs->xds = __USER_DS;               \        regs->xes = __USER_DS;               \        regs->xss = __USER_DS;               \        regs->xcs = __USER_CS;               \        regs->eip = new_eip;                \        regs->esp = new_esp;                \ } while (0) *************************************************** 这几条指令把作为参数传下来的用户空间程序入口和堆栈指针设置到regs数据结构中,这个数据结构实际上在系统堆栈中,是在当前进程通过系统调用进入内核时由SAVE_ALL形成的,而指向所保存现场的指针regs则作为参数传给了sys_execve(),并逐层传了下来。把所保存现场中的eip和esp改成了新的地址,就使得CPU在返回用户空间时进入新的程序入口。如果有dynamic linker映像存在,那么这就是dynamic linker映像的程序入口,否则就是目标映像的程序入口。

23、如果gemfield这个进程是被追踪的, 它将通知调试器execve()系统调用已经完成;

24、如果成功的话,返回零值。

第十七步、回到用户态

当返回到用户态后,因为EIP寄存器的值在22中已经被设置为dynamic linker的入口点,于是程序开始从dynamic linker启动,最终新的进程开始运行。

有一种落差是,你配不上自己的野心,也辜负了所受的苦难
posted @ 2017-08-20 13:52  专注it  阅读(246)  评论(0编辑  收藏  举报