本文将介绍linux程序的执行过程,并以实际问题为切入点简单介绍下ELF程序的加载过程。
【正文】用后态执行
我们知道在linux系统中可以通过诸如"./debug"方式执行一个程序,那么这个程序的执行过程中linux系统都做了什么?
本文以debug程序为例,介绍linux内核是如何一步步将debug进程执行起来的.
1 执行过程:
以system()实现为例,它是一种典型的可执行程序运行过程:
[cpp] view plain copy
#include
#include
#include
#include
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid = 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0)
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
观察上面system实现:
1)system在当前进程中fork创建了一个子进程,并执行execl函数运行可执行文件;
2)execl/execve系列函数执行elf文件;
实际上系统通过execve->do_execve_common函数,将上步创建的子进程,完全替换成了可执行程序.
这个替换过程,其实也就是可执行程序的加载过程,也是本文着重介绍的内容.
3) execve使用实例:
#include
int execve(const char *filename, char *const argv[], char *const envp[]);
[cpp] view plain copy
#include
#include
int main(int arg, char **args)
{
char *argv[]={"ls","-al","/home/", NULL};
char *envp[]={0,NULL};
execve("/bin/ls",argv,envp);
}
【正文】内核态执行
linux系统中,可执行程序大多属于ELF文件格式.
本节以实例介绍:execve("/home/debug",NULL,NULL);其中debug程序是elf格式.
当用后执行execve时,系统都做了什么?下面逐层分析:
1 系统调用:execve->do_execve->do_execve_common
[cpp] view plain copy
/* filename为可执行文件:/home/debug;
argv为NULL,表示可行程序不带参数;
envp为NULL,表示没有指定环境变量; */
int do_execve(const char *filename,const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.naTIve = __envp };
return do_execve_common(filename, argv, envp);
}
2 execve->do_execve->do_execve_common()注意此时当前进程是上文中创建的子进程。
bprm_mm_init()完成进程地址空间vma(包括栈)的初始化.
[cpp] view plain copy
/*
* sys_execve() executes a new program.
*/
staTIc int do_execve_common(const char *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
/*注意linux_binprm是核心数据结构,它保存了可执行文件的信息;*/
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
bool clear_in_exec;
int retval;
const struct cred *cred = current_cred();
/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don't check setuid() return code. Here we addiTIonally recheck
* whether NPROC limit is sTIll exceeded.
*/
if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(&cred->user->processes) > rlimit(RLIMIT_NPROC)) {
retval = -EAGAIN;
goto out_ret;
}
/* We're below the limit (still or again), so we don't want to make
* further execve() calls fail. */
current->flags &= ~PF_NPROC_EXCEEDED;
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
retval = -ENOMEM;
/*申请linux_binprm描述符,用以保存ELF可执行文件信息*/
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
/*生成bprm->cred即准备可执行程序运行的用户和组信息,主要根据当前进程的task->cred信息生成*/
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
retval = check_unsafe_exec(bprm);
if (retval
goto out_free;
clear_in_exec = retval;
current->in_execve = 1;
/*
1:打开可执行程序 /home/debug;
*/
file = open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
/*bprm->file为/home/debug文件描述符*/
bprm->file = file;
/*可执行文件名保存到bprm->filename中*/
bprm->filename = filename;
bprm->interp = filename;
/*生成bprm->mm,即准备可执行程序的mm_struct信息,
注意此时生成栈空间信息,不过后面会对栈空间再次调整
注意此处的bprm->mm不是当前进程的,是bprm_mm_init申请的
以后用作/home/debug进程的mm_struct;
*/
retval = bprm_mm_init(bprm);
if (retval)
goto out_file;
/*可执行文件参数个数,对/home/debug来说argc=0,因为指定参数为NULL*/
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc)
goto out;
/*envc=0参加bprm->argc*/
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc)
goto out;
/*
elf头保存到bprm->buf中;
实现方式: kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);//128bytes
*/
retval = prepare_binprm(bprm);
if (retval
goto out;
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval
goto out;
/*保存execve中指定的环境变量到linux_binprm结构中*/
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);
if (retval
goto out;
/*保存execve中指定的可执行程序参数到linux_binprm结构中*/
retval = copy_strings(bprm->argc, argv, bprm);
if (retval
goto out;
/*
该函数负责从flash上加载ELF文件:并将当前子进程信息替换为可执行文件中读取的信息.
elf_format->load_binary=load_elf_binary->arch_setup_additional_pages : register_binfmt中注册的elf_format
->install_special_mapping->insert_vm_struct
*/
retval = search_binary_handler(bprm);
if (retval
goto out;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
acct_update_integrals(current);
free_bprm(bprm);
if (displaced)
put_files_struct(displaced);
return retval;
out:
if (bprm->mm) {
acct_arg_size(bprm, 0);
mmput(bprm->mm);
}
out_file:
if (bprm->file) {
allow_write_access(bprm->file);
fput(bprm->file);
}
out_unmark:
if (clear_in_exec)
current->fs->in_exec = 0;
current->in_execve = 0;
out_free:
free_bprm(bprm);
out_files:
if (displaced)
reset_files_struct(displaced);
out_ret:
return retval;
}
2.1 ELF头读取过程:do_execve_common()->prepare_binprm()
[cpp] view plain copy
int prepare_binprm(struct linux_binprm *bprm)
{
umode_t mode;
struct inode * inode = file_inode(bprm->file);
int retval;
mode = inode->i_mode;
if (bprm->file->f_op == NULL)
return -EACCES;
/* clear any previous set[ug]id data from a previous binary */
bprm->cred->euid = current_euid();
bprm->cred->egid = current_egid();
if (!(bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID) &&
!current->no_new_privs &&
kuid_has_mapping(bprm->cred->user_ns, inode->i_uid) &&
kgid_has_mapping(bprm->cred->user_ns, inode->i_gid)) {
/* Set-uid? */
if (mode & S_ISUID) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->euid = inode->i_uid;
}
/* Set-gid? */
/*
* If setgid is set but no group execute bit then this
* is a candidate for mandatory locking, not a setgid
* executable.
*/
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->egid = inode->i_gid;
}
}
/* fill in binprm security blob */
retval = security_bprm_set_creds(bprm);
if (retval)
return retval;
bprm->cred_prepared = 1;
memset(bprm->buf, 0, BINPRM_BUF_SIZE);
/*
elf头保存到bprm->buf中
*/
return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}
[ELF文件加载]
ELF文件格式:https://baike.baidu.com/item/ELF/7120560?fr=aladdin
3.1文件头(Elf header) :
Elf头在程序的开始部位,作为引路表描述整个ELF的文件结构,其信息大致分为四部分:一是系统相关信息,二是目标文件类型,三是加载相关信息,四是链接相关信息。
其中系统相关信息包括elf文件魔数(标识elf文件),平台位数,数据编码方式,elf头部版本,硬件平台e_machine,目标文件版本 e_version,处理器特定标志e_ftags:这些信息的引入极大增强了elf文件的可移植性,使交叉编译成为可能。目标文件类型用e_type的值表示,可重定位文件为1,可执行文件为2,共享文件为3;加载相关信息有:程序进入点e_entry.程序头表偏移量e_phoff,elf头部长度 e_ehsize,程序头表中一个条目的长度e_phentsize,程序头表条目数目e_phnum;链接相关信息有:节头表偏移量e_shoff,节头表中一个条目的长度e_shentsize,节头表条目个数e_shnum ,节头表字符索引e shstmdx。可使用命令"readelf -h filename"来察看文件头的内容。
文件头的数据结构如下:
[cpp] view plain copy
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;//目标文件类型
Elf32_Half e_machine;//硬件平台
Elf32_Word e_version;//elf头部版本
Elf32_Addr e_entry;//程序进入点
<