ELF 실행되는 과정 요약

Source Code 분석
먼저 실행가능한 파일(ELF)에 대한 정보를 담게될 linux_binprm에 대해서 살펴보자.
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm {
char buf[BINPRM_BUF_SIZE]; // ELF Header의 128개 문자르 저장한다
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
#endif
struct mm_struct *mm;
unsigned long p; /* current top of mem, 메모리 페이지의 제일 높은 주소 */
unsigned int
cred_prepared:1,/* true if creds already prepared (multiple
* preps happen for interpreters) */
cap_effective:1;/* true if has elevated effective capabilities,
* false if not; except for init which inherits
* its parent's caps anyway */
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth; /* only for search_binary_handler() */
struct file * file; // 실행할 ELF File
struct cred *cred; /* new credentials */
int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
unsigned int per_clear; /* bits to clear in current->personality */
int argc, envc; // 인자와 환경변수
const char * filename; /* Name of binary as seen by procps */
const char * interp; /* Name of the binary really executed. Most
of the time same as filename, but could be
different for binfmt_{misc,script} */ // 실행할 파일의 진짜 이름, 보통은 filename과 같다
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
Linux는 여러가지 실행가능한 파일 type을 제공한다. 실행 가능한 file들에 대한 정보는 linux_binfmt 구조체에 등록되어 있고, 리스트 형태로 관리된다.
register_binfmt( ) 함수를 통하여 시스템 초기화 시, 각 파일 포맷에 맞는 handler가 등록된다.
아래 글에서 세부적인 내용을 다루고 여기서는 구조체의 형태만 한번 보도록 하자.
/*
* This structure defines the functions that are used to load the binary formats that
* linux accepts.
*/
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
};

ELF 실행의 함수 Call Tree는 위 그림과 같다.
아래 부터는 위 그림의 내용을 하나씩 따라가 본다.
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
ELF 파일이 처음으로 실행되면 SYSCALL_DEFINE3 macro define을 이용하여 do_execve가 실행된다.
getname( ) 함수는 다시 getname_flags( ) 함수를 호출하고, memory 공간을 할당(kzalloc)받아 user space에서 부터 전달된 filename을 저장한다.
int do_execve(struct filename *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_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}
user_arg_ptr 구조체는 user space의 data를 encapsulation 하는데 사용되어지는 구조체이다. process가 사용하게 될 인자인 __argv와 __envp를 encapsulation 한다.
AT_FDCWD는 file open이 root directory가 아닌 현재 directory에서 진행되도록 하는 flag 이다.
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
char *pathbuf = NULL;
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
int retval;
// IS_ERR macro를 이용하여 filename의 유효성을 검사한다.
// 만약 filename이 가르키고 있는 포인터가
// kernel space의 제일 마지막 page를 가르키고 있다면
// ERR로 판단하고 함수를 종료한다.
if (IS_ERR(filename))
return PTR_ERR(filename);
/*
* 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.
*/
// PF_NPROC_EXCCEEDED는 process의 갯수가 최대 갯수인 RLIMIT_NPROC를 초과하는 경우 set 된다.
// 현재 상태가 더이상 process를 새롭게 만들 수 없는 상황이라면 함수를 종료한다.
if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(¤t_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;
// unshare_files( ) 함수를 이용하여 이전 process의 file list pointer를 displaced에 저장한다.
// 만약 이전 process의 file_struct 구조체에서 error를 발견시에는
// 이전 file list를 복구하는데 사용된다.
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
// linux_binprm 구조체를 할당받는다.
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
// linux_binprm 구조체에 대하여 초기화 진행.
// 이전 process로 부터 cred 구조체를 복사하고, 인증정보를 가져온다.
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
// process가 인증받은 안전한 상태인지 확인하고,
// 만약 unsafe 상태라면 linux_binprm->unsafe에 값을 설정한다.
check_unsafe_exec(bprm);
current->in_execve = 1;
// 내부적으로 do_flip_open( ) 함수를 호출하여 file을 열고, file 구조체를 반환한다.
file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
// 함수는 multi core 환경에서, 현재 가장 부하가 적은 core를 찾아 binary를 수행한다.
sched_exec();
bprm->file = file;
if (fd == AT_FDCWD || filename->name[0] == '/') {
bprm->filename = filename->name;
} else {
if (filename->name[0] == '\0')
pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
else
pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
fd, filename->name);
if (!pathbuf) {
retval = -ENOMEM;
goto out_unmark;
}
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. Relies on having exclusive access to
* current->files (due to unshare_files above).
*/
if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
bprm->filename = pathbuf;
}
bprm->interp = bprm->filename;
// 새로운 process 생성에 쓰일 mm_struct 를 할당한다.
retval = bprm_mm_init(bprm);
if (retval)
goto out_unmark;
// 인자와 환경변수의 길이를 계산하여 bprm->argc, bprm->envc에 대입한다.
// count( ) 함수 내부에서는 get_user( ) 함수를 이용하여 user space pointer의 유효셩을 검사한다.
// (thread_info->addr_limit을 통해서 확인한다.
// get_user( )는 kernel에서 user space의 값을 읽어오는 API이다.)
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc) < 0)
goto out;
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc) < 0)
goto out;
// process의 권한을 설정하고, ELF 파일의 내용을 읽어서 bprm->buf의 cache에 저장한다.
retval = prepare_binprm(bprm);
if (retval < 0)
goto out;
// copy_strings_kernel( )과 copy_strings( ) 함수를 이용하여
// user space의 data를 kernel space로 복사한다.
// 또한 내부적으로는 kmap( )을 이용하여 kernel space의 page를 할당받는다.
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0)
goto out;
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out;
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out;
// 새로운 process를 시작한다.
retval = exec_binprm(bprm);
if (retval < 0)
goto out;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
acct_update_integrals(current);
task_numa_free(current);
free_bprm(bprm);
kfree(pathbuf);
putname(filename);
if (displaced)
put_files_struct(displaced);
return retval;
out:
if (bprm->mm) {
acct_arg_size(bprm, 0);
mmput(bprm->mm);
}
out_unmark:
current->fs->in_exec = 0;
current->in_execve = 0;
out_free:
free_bprm(bprm);
kfree(pathbuf);
out_files:
if (displaced)
reset_files_struct(displaced);
out_ret:
putname(filename);
return retval;
}
IS_ERR macro를 이용하여 filename의 유효성을 검사한다. 만약 filename이 가르키고 있는 포인터가 kernel space의 제일 마지막 page를 가르키고 있다면 ERR로 판단하고 함수를 종료한다.
PF_NPROC_EXCCEEDED는 process의 갯수가 최대 갯수인 RLIMIT_NPROC를 초과하는 경우 set 된다. 현재 상태가 더이상 process를 새롭게 만들 수 없는 상황이라면 함수를 종료한다.
unshare_files( ) 함수를 이용하여 이전 process의 file list pointer를 displaced에 저장한다. 만약 이전 process의 file_struct 구조체에서 error를 발견시에는 이전 file list를 복구하는데 사용된다.
linux_binprm 구조체를 할당받고, 해당 구조체에 대한 초기화를 진행한다.
prepare_bprm_creds( ) 함수는 이전 process로 부터 cred 구조체를 복사하고, 인증정보를 가져온다.
check_unsafe_exec( )를 통하여 process가 인증받은 안전한 상태인지 확인하고, 만약 unsafe 상태라면 linux_binprm->unsafe에 값을 설정한다.
do_open_execat( ) 함수는 do_flip_open( ) 함수를 호출하여 file을 열고, file 구조체를 반환한다.
sched_exec( ) 함수는 multi core 환경에서, 현재 가장 부하가 적은 core를 찾아 binary를 수행한다.
bprm_mm_init( ) 함수를 이용하여 새로운 process 생성에 쓰일 mm_struct 를 할당한다.
인자와 환경변수의 길이를 계산하여 bprm->argc, bprm->envc에 대입한다. count( ) 함수 내부에서는 get_user( ) 함수를 이용하여 user space pointer의 유효셩을 검사한다. (thread_info->addr_limit을 통해서 확인한다. get_user( )는 kernel에서 user space의 값을 읽어오는 API이다.)
prepare_binprm( )에서는 process의 권한을 설정하고, ELF 파일의 내용을 읽어서 bprm->buf의 cache에 저장한다.
copy_strings_kernel( )과 copy_strings( ) 함수를 이용하여 user space의 data를 kernel space로 복사한다. 또한 내부적으로는 kmap( )을 이용하여 kernel space의 page를 할당받는다.
마지막으로, exec_binprm( )을 호출하여 새로운 process를 시작한다.
/*
* Helper to unshare the files of the current task.
* We don't want to expose copy_files internals to
* the exec layer of the kernel.
*/
int unshare_files(struct files_struct **displaced)
{
struct task_struct *task = current;
struct files_struct *copy = NULL;
int error;
error = unshare_fd(CLONE_FILES, ©);
if (error || !copy) {
*displaced = NULL;
return error;
}
*displaced = task->files;
task_lock(task);
task->files = copy;
task_unlock(task);
return 0;
}
/*
* Unshare file descriptor table if it is being shared
*/
static int unshare_fd(unsigned long unshare_flags, struct files_struct **new_fdp)
{
struct files_struct *fd = current->files;
int error = 0;
if ((unshare_flags & CLONE_FILES) &&
(fd && atomic_read(&fd->count) > 1)) {
*new_fdp = dup_fd(fd, &error);
if (!*new_fdp)
return error;
}
return 0;
}
unshare_files( )은 unshare_fd( )함수를 통하여 직전 process인 current의 file list를 displaced에 복사해 온다.
해당 조작은 dup_fd( )함수를 통하여 이루어 진다.
/*
* Prepare credentials and lock ->cred_guard_mutex.
* install_exec_creds() commits the new creds and drops the lock.
* Or, if exec fails before, free_bprm() should release ->cred and
* and unlock.
*/
int prepare_bprm_creds(struct linux_binprm *bprm)
{
if (mutex_lock_interruptible(¤t->signal->cred_guard_mutex))
return -ERESTARTNOINTR;
bprm->cred = prepare_exec_creds();
if (likely(bprm->cred))
return 0;
mutex_unlock(¤t->signal->cred_guard_mutex);
return -ENOMEM;
}
struct cred *prepare_exec_creds(void)
{
struct cred *new;
new = prepare_creds();
if (!new)
return new;
#ifdef CONFIG_KEYS
/* newly exec'd tasks don't get a thread keyring */
key_put(new->thread_keyring);
new->thread_keyring = NULL;
/* inherit the session keyring; new process keyring */
key_put(new->process_keyring);
new->process_keyring = NULL;
#endif
return new;
}
/**
* prepare_creds - Prepare a new set of credentials for modification
*
* Prepare a new set of task credentials for modification. A task's creds
* shouldn't generally be modified directly, therefore this function is used to
* prepare a new copy, which the caller then modifies and then commits by
* calling commit_creds().
*
* Preparation involves making a copy of the objective creds for modification.
*
* Returns a pointer to the new creds-to-be if successful, NULL otherwise.
*
* Call commit_creds() or abort_creds() to clean up.
*/
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;
validate_process_creds();
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_creds() alloc %p", new);
old = task->cred;
memcpy(new, old, sizeof(struct cred));
atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);
#ifdef CONFIG_KEYS
key_get(new->session_keyring);
key_get(new->process_keyring);
key_get(new->thread_keyring);
key_get(new->request_key_auth);
#endif
#ifdef CONFIG_SECURITY
new->security = NULL;
#endif
if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
goto error;
validate_creds(new);
return new;
error:
abort_creds(new);
return NULL;
}
EXPORT_SYMBOL(prepare_creds);
이전 process의 cred 구조체를 복사해 오고 reference counter를 증가시킨다.
/*
* determine how safe it is to execute the proposed program
* - the caller must hold ->cred_guard_mutex to protect against
* PTRACE_ATTACH or seccomp thread-sync
*/
static void check_unsafe_exec(struct linux_binprm *bprm)
{
struct task_struct *p = current, *t;
unsigned n_fs;
if (p->ptrace) {
if (p->ptrace & PT_PTRACE_CAP)
bprm->unsafe |= LSM_UNSAFE_PTRACE_CAP;
else
bprm->unsafe |= LSM_UNSAFE_PTRACE;
}
/*
* This isn't strictly necessary, but it makes it harder for LSMs to
* mess up.
*/
if (task_no_new_privs(current))
bprm->unsafe |= LSM_UNSAFE_NO_NEW_PRIVS;
t = p;
n_fs = 1;
spin_lock(&p->fs->lock);
rcu_read_lock();
while_each_thread(p, t) {
if (t->fs == p->fs)
n_fs++;
}
rcu_read_unlock();
if (p->fs->users > n_fs)
bprm->unsafe |= LSM_UNSAFE_SHARE;
else
p->fs->in_exec = 1;
spin_unlock(&p->fs->lock);
}
ptrace는 debugging 정보가 포함된 ELF의 경우 설정된다.
task_no_new_privs( ) macro는 include/linux/sched.h에 정의되어 있으며, task_struct->atomic_flags가 PFA_NO_NEW_PRIVS가 설정되어 있는지 검사한다. (새로운 권한을 획득하지 않음을 나타낸다.)
그 다음 process내의 thread 中 fs_struct가 동일한 thread의 갯수를 새어, 현재 process내의 fs_struct user 갯수와 비교한다. 만약 fs_struct user 갯수가 더 많다면, bprm->unsafe에 LSM_UNSAFE_SHARE를 설정한다. 만약에 작다면 current->fs->in_exec에 1을 설정한다.
static struct file *do_open_execat(int fd, struct filename *name, int flags)
{
struct file *file;
int err;
struct open_flags open_exec_flags = {
.open_flag = O_LARGEFILE | O_RDONLY | __FMODE_EXEC,
.acc_mode = MAY_EXEC | MAY_OPEN,
.intent = LOOKUP_OPEN,
.lookup_flags = LOOKUP_FOLLOW,
};
if ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)) != 0)
return ERR_PTR(-EINVAL);
if (flags & AT_SYMLINK_NOFOLLOW)
open_exec_flags.lookup_flags &= ~LOOKUP_FOLLOW;
if (flags & AT_EMPTY_PATH)
open_exec_flags.lookup_flags |= LOOKUP_EMPTY;
file = do_filp_open(fd, name, &open_exec_flags);
if (IS_ERR(file))
goto out;
err = -EACCES;
if (!S_ISREG(file_inode(file)->i_mode))
goto exit;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC)
goto exit;
err = deny_write_access(file);
if (err)
goto exit;
if (name->name[0] != '\0')
fsnotify_open(file);
out:
return file;
exit:
fput(file);
return ERR_PTR(err);
}
ELF 파일을 열기 위하여 필요한 작업들을 수행한다.
/*
* sched_exec - execve() is a valuable balancing opportunity, because at
* this point the task has the smallest effective memory and cache footprint.
*/
void sched_exec(void)
{
struct task_struct *p = current;
unsigned long flags;
int dest_cpu;
raw_spin_lock_irqsave(&p->pi_lock, flags);
dest_cpu = p->sched_class->select_task_rq(p, task_cpu(p), SD_BALANCE_EXEC, 0);
if (dest_cpu == smp_processor_id())
goto unlock;
if (likely(cpu_active(dest_cpu))) {
struct migration_arg arg = { p, dest_cpu };
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
stop_one_cpu(task_cpu(p), migration_cpu_stop, &arg);
return;
}
unlock:
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
}
select_task_rq( )함수를 통하여 load가 가정 적은 cpu를 선택한다.
stop_one_cpu( )를 이용하여 선택된 cpu를 멈추고 문맥교환을 진행한다.
/*
* Create a new mm_struct and populate it with a temporary stack
* vm_area_struct. We don't have enough context at this point to set the stack
* flags, permissions, and offset, so we use temporary values. We'll update
* them later in setup_arg_pages().
*/
static int bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct mm_struct *mm = NULL;
bprm->mm = mm = mm_alloc();
err = -ENOMEM;
if (!mm)
goto err;
err = __bprm_mm_init(bprm);
if (err)
goto err;
return 0;
err:
if (mm) {
bprm->mm = NULL;
mmdrop(mm);
}
return err;
}
static int __bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct vm_area_struct *vma = NULL;
struct mm_struct *mm = bprm->mm;
bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma)
return -ENOMEM;
down_write(&mm->mmap_sem);
vma->vm_mm = mm;
/*
* Place the stack at the largest stack address the architecture
* supports. Later, we'll move this to an appropriate place. We don't
* use STACK_TOP because that can depend on attributes which aren't
* configured yet.
*/
BUILD_BUG_ON(VM_STACK_FLAGS & VM_STACK_INCOMPLETE_SETUP);
vma->vm_end = STACK_TOP_MAX;
vma->vm_start = vma->vm_end - PAGE_SIZE;
vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
INIT_LIST_HEAD(&vma->anon_vma_chain);
err = insert_vm_struct(mm, vma);
if (err)
goto err;
mm->stack_vm = mm->total_vm = 1;
arch_bprm_mm_init(mm, vma);
up_write(&mm->mmap_sem);
bprm->p = vma->vm_end - sizeof(void *);
return 0;
err:
up_write(&mm->mmap_sem);
bprm->vma = NULL;
kmem_cache_free(vm_area_cachep, vma);
return err;
}
bprm_mm_init( ) 함수는 먼저 mm_alloc( )을 통하여 새로운 process의 mm_struct를 할당받는다. 다음에는 __bprm_mm_init( )을 통하여 초기화를 진행한다.
__bprm_mm_init( )에서는 virtual address space를 관리하는 구조체안 vm_area_struct를 할당받는다. 그 다음에는 각각의 member 변수에 대한 초기화를 진행한다.
이 때, virtual address의 마지막을 나타내는 vm_end에 STACK_TOP_MAX 값을 대입한다.
(MMU가 활성화 되어 있는 32bit system의 경우 :
STACK_TOP_MAX = TASK_SIZE = CONFIG_PAGEOFFSET - SZ_16M = 0xC0000000 - 0x01000000 = 0xBF000000 ==> process가 사용 가능한 최대 크기의 가상주소 공간을 말한다)
insert_vm_struct( ) 함수는 할당받은 virtual address space인 vm_area_struct를 memory 관리 system인 mm_struct에 입력한다.
arch_bprm_mm_init( ) 함수를 통하여 해당 프로세서에 맞는 초기화를 진행한다.
마지막으로, stack pointer를 설정하는데 STACK_TOP_MAX에서 pointer 크기 만큼을 뺀 값이 stack pointer가 된다.
/*
* Fill the binprm structure from the inode.
* Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
*
* This may be called multiple times for binary chains (scripts for example).
*/
int prepare_binprm(struct linux_binprm *bprm)
{
int retval;
bprm_fill_uid(bprm);
/* 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);
return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}
EXPORT_SYMBOL(prepare_binprm);
bprm_fill_uid( )는 process의 uid와 gid를 설정한다. 그 다음, security_bprm_set_creds( ) 함수를 통하여 권한을 설정한다. 마지막으로, kernel_read( ) 함수를 통하여 ELF file의 내용을 읽어서 bprm의 cache buf에 기록한다.
static void bprm_fill_uid(struct linux_binprm *bprm)
{
struct inode *inode;
unsigned int mode;
kuid_t uid;
kgid_t gid;
/* 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)
return;
if (task_no_new_privs(current))
return;
inode = file_inode(bprm->file);
mode = READ_ONCE(inode->i_mode);
if (!(mode & (S_ISUID|S_ISGID)))
return;
/* Be careful if suid/sgid is set */
mutex_lock(&inode->i_mutex);
/* reload atomically mode/uid/gid now that lock held */
mode = inode->i_mode;
uid = inode->i_uid;
gid = inode->i_gid;
mutex_unlock(&inode->i_mutex);
/* We ignore suid/sgid if there are no mappings for them in the ns */
if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||
!kgid_has_mapping(bprm->cred->user_ns, gid))
return;
if (mode & S_ISUID) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->euid = uid;
}
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->egid = gid;
}
}
먼저 euid와 egid를 직전 process의 euid와 egid로 설정한다.
그 다음, file의 mode 中, S_ISGID와 S_ISUID에 해당하는 bit를 검사하여 실행가능한 파일인지 검사한다. 만약 실행가능한 파일이면 해당하는 bit가 1로 set되어 있다.
파일이 실행 중에는 process의 effective group ID(egid)와 effective user ID(euid)가 file에 기록되어 있는 i_uid와 i_gid로 설정된다.
static int exec_binprm(struct linux_binprm *bprm)
{
pid_t old_pid, old_vpid;
int ret;
/* Need to fetch pid before load_binary changes it */
old_pid = current->pid;
rcu_read_lock();
old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
rcu_read_unlock();
ret = search_binary_handler(bprm);
if (ret >= 0) {
audit_bprm(bprm);
trace_sched_process_exec(current, old_pid, bprm);
ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
proc_exec_connector(current);
}
return ret;
}
int search_binary_handler(struct linux_binprm *bprm)
{
bool need_retry = IS_ENABLED(CONFIG_MODULES);
struct linux_binfmt *fmt;
int retval;
/* This allows 4 levels of binfmt rewrites before failing hard. */
if (bprm->recursion_depth > 5)
return -ELOOP;
retval = security_bprm_check(bprm);
if (retval)
return retval;
retval = -ENOENT;
retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
bprm->recursion_depth--;
if (retval < 0 && !bprm->mm) {
/* we got to flush_old_exec() and failed after it */
read_unlock(&binfmt_lock);
force_sigsegv(SIGSEGV, current);
return retval;
}
if (retval != -ENOEXEC || !bprm->file) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
if (need_retry) {
if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
printable(bprm->buf[2]) && printable(bprm->buf[3]))
return retval;
if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
return retval;
need_retry = false;
goto retry;
}
return retval;
}
EXPORT_SYMBOL(search_binary_handler);
exec_binprm( )은 search_binary_handler( )을 통하여 실행 가능한 파일을 조작한다.
미리 등록되어 있는 format handler들을 순회하면서 인식 가능한 파일인지 검사하고, load_binary( )를 실행한다.
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);
return 0;
}
ELF 파일의 경우 format handler에 기술되어 있는 대로, load_elf_binary를 실행한다.
static int load_elf_binary(struct linux_binprm *bprm)
{
struct file *interpreter = NULL; /* to shut gcc up */
unsigned long load_addr = 0, load_bias = 0;
int load_addr_set = 0;
char * elf_interpreter = NULL;
unsigned long error;
struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
unsigned long elf_bss, elf_brk;
int retval, i;
unsigned long elf_entry;
unsigned long interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
unsigned long reloc_func_desc __maybe_unused = 0;
int executable_stack = EXSTACK_DEFAULT;
struct pt_regs *regs = current_pt_regs();
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
} *loc;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
if (!loc) {
retval = -ENOMEM;
goto out_ret;
}
/* Get the exec-header */
loc->elf_ex = *((struct elfhdr *)bprm->buf);
retval = -ENOEXEC;
/* First of all, some simple consistency checks */
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out;
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
if (!elf_check_arch(&loc->elf_ex))
goto out;
if (!bprm->file->f_op->mmap)
goto out;
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
if (!elf_phdata)
goto out;
......
load_elf_binary( )의 첫번째 부분은 ELF header의 주요 내용들을 검사하는 것이다.
먼저 e_indent라고 부르는 magic number를 확인한다.
그 다음에는 e_type이 ET_EXEC, EX_DYN이 아니면 실행을 계속한다.
int elf_check_arch(const struct elf32_hdr *x)
{
unsigned int eflags;
/* Make sure it's an ARM executable */
if (x->e_machine != EM_ARM)
return 0;
/* Make sure the entry address is reasonable */
if (x->e_entry & 1) {
if (!(elf_hwcap & HWCAP_THUMB))
return 0;
} else if (x->e_entry & 3)
return 0;
eflags = x->e_flags;
if ((eflags & EF_ARM_EABI_MASK) == EF_ARM_EABI_UNKNOWN) {
unsigned int flt_fmt;
/* APCS26 is only allowed if the CPU supports it */
if ((eflags & EF_ARM_APCS_26) && !(elf_hwcap & HWCAP_26BIT))
return 0;
flt_fmt = eflags & (EF_ARM_VFP_FLOAT | EF_ARM_SOFT_FLOAT);
/* VFP requires the supporting code */
if (flt_fmt == EF_ARM_VFP_FLOAT && !(elf_hwcap & HWCAP_VFP))
return 0;
}
return 1;
}
EXPORT_SYMBOL(elf_check_arch);
elf_check_arch( )는 e_machine, e_entry, e_flags 값을 확인한다. 또한, entry address 유효성, Vector Floating Point 지원 여부등을 검사한다.
elf_check_arch( )의 구현은 architecture 별로 상이하다.
load_elf_phdr( ) 함수는 ELF File내의 Segment에 관한 정보를 가지고 있는 Program Header를 읽어서 elf_phdata에 저장한다.
Elf32_Ehdr 및 Elf32_Phdr에 대한 구조체는 아래와 같다.
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
해당 구조체에 대한 자세한 설명은 본 글 아래의 링크를 참조하기 바란다.
/**
* load_elf_phdrs() - load ELF program headers
* @elf_ex: ELF header of the binary whose program headers should be loaded
* @elf_file: the opened ELF binary file
*
* Loads ELF program headers from the binary file elf_file, which has the ELF
* header pointed to by elf_ex, into a newly allocated array. The caller is
* responsible for freeing the allocated data. Returns an ERR_PTR upon failure.
*/
static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,
struct file *elf_file)
{
struct elf_phdr *elf_phdata = NULL;
int retval, size, err = -1;
/*
* If the size of this structure has changed, then punt, since
* we will be doing the wrong thing.
*/
if (elf_ex->e_phentsize != sizeof(struct elf_phdr))
goto out;
/* Sanity check the number of program headers... */
if (elf_ex->e_phnum < 1 ||
elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr))
goto out;
/* ...and their total size. */
size = sizeof(struct elf_phdr) * elf_ex->e_phnum;
if (size > ELF_MIN_ALIGN)
goto out;
elf_phdata = kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;
/* Read in the program headers */
retval = kernel_read(elf_file, elf_ex->e_phoff,
(char *)elf_phdata, size);
if (retval != size) {
err = (retval < 0) ? retval : -EIO;
goto out;
}
/* Success! */
err = 0;
out:
if (err) {
kfree(elf_phdata);
elf_phdata = NULL;
}
return elf_phdata;
}
ELF File의 Header에서 e_phnum은 segment의 갯수를 나타내고, e_phentsize는 program header의 크기를 나타낸다.
e_phoff는 program header가 file 내에서 어디에 위치하는지를 나타난다.
kernel_read( ) 함수를 이용하여 file내의 program header를 읽어서 elf_phdata 구조체에 저장한다.
static int load_elf_binary(struct linux_binprm *bprm)
{
......
elf_ppnt = elf_phdata;
elf_bss = 0;
elf_brk = 0;
start_code = ~0UL;
end_code = 0;
start_data = 0;
end_data = 0;
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
/* This is the program interpreter used for
* shared libraries - for now assume that this
* is an a.out format binary
*/
retval = -ENOEXEC;
if (elf_ppnt->p_filesz > PATH_MAX ||
elf_ppnt->p_filesz < 2)
goto out_free_ph;
retval = -ENOMEM;
elf_interpreter = kmalloc(elf_ppnt->p_filesz,
GFP_KERNEL);
if (!elf_interpreter)
goto out_free_ph;
retval = kernel_read(bprm->file, elf_ppnt->p_offset,
elf_interpreter,
elf_ppnt->p_filesz);
if (retval != elf_ppnt->p_filesz) {
if (retval >= 0)
retval = -EIO;
goto out_free_interp;
}
/* make sure path is NULL terminated */
retval = -ENOEXEC;
if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
goto out_free_interp;
interpreter = open_exec(elf_interpreter);
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_interp;
/*
* If the binary is not readable then enforce
* mm->dumpable = 0 regardless of the interpreter's
* permissions.
*/
would_dump(bprm, interpreter);
retval = kernel_read(interpreter, 0, bprm->buf,
BINPRM_BUF_SIZE);
if (retval != BINPRM_BUF_SIZE) {
if (retval >= 0)
retval = -EIO;
goto out_free_dentry;
}
/* Get the exec headers */
loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);
break;
}
elf_ppnt++;
}
......
load_elf_binary( )의 두번째 부분은 ELF File 내에서 interpreter의 정보를 읽어온다.
먼저, program header의 모든 부분은 탐색하면서 p_type값이 PT_INTERP인지 확인한다.
interpreter에 대해서는, p_filesz는 해당 library 까지의 path를 말한다. (ex. /usr/lib/gcc/x64-64-linux-gnu/7/libgcc_s.so)
p_offset은 ELF File 내부에서 path가 위치해 있는 공간을 가리킨다.
p_filesz와 p_offset 값의 유효성 여부에 대해서 검사하는 부분이 들어가 있다.
p_filesz 만큼의 공간을 할당받아서, kernel_read( )를 이용하여 interpreter의 path를 elf_interpreter에 저장한다.
다음으로, open_exec( )함수를 통해서 interpreter를 열어서 file 구조체를 반환한다. openc_exec( ) 함수 또한 내부적으로는 do_open_execat( ) 함수를 사용한다.
kernel_read( ) 함수를 사용하여 interpreter의 첫번째 128개의 문자를 bprm->buf에 저장한다. 이전에 ELF File header의 내용이 buffer에 쓰여져 있었으므로 overwrite 된다.
마지막으로, bprm->buf 저장되어 있는 interpreter 시작주소를 loc->interp_elf_ex에 저장한다.
static int load_elf_binary(struct linux_binprm *bprm)
{
......
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,
bprm->file, false,
&arch_state);
if (retval)
goto out_free_dentry;
break;
}
......
PT_GNU_STACK segment를 찾아서 stack의 실행가능 여부를 판단한다.
stack의 실행가능 여부에 따라서 executable_stack 값을 설정한다.
(executable stack : stack 영역에 code를 적재하여 실행하는 것을 말한다)
PT_LOPROC와 PT_HOPROC는 개별 processor를 위하여 reserve 되어 있는 segment이다.
/* Some simple consistency checks for the interpreter */
if (elf_interpreter) {
retval = -ELIBBAD;
/* Not an ELF interpreter */
if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out_free_dentry;
/* Verify the interpreter has a valid arch */
if (!elf_check_arch(&loc->interp_elf_ex))
goto out_free_dentry;
/* Load the interpreter program headers */
interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
interpreter);
if (!interp_elf_phdata)
goto out_free_dentry;
/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
elf_ppnt = interp_elf_phdata;
for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(&loc->interp_elf_ex,
elf_ppnt, interpreter,
true, &arch_state);
if (retval)
goto out_free_dentry;
break;
}
}
path가 제공된 interpreter 또한 ELF 이다.
그러므로 마찬가지로 load_elf_phdrs( ) 함수를 통하여 interpreter의 program header를 읽어온다.
static int load_elf_binary(struct linux_binprm *bprm)
{
......
/*
* Allow arch code to reject the ELF at this point, whilst it's
* still possible to return an error to the code that invoked
* the exec syscall.
*/
retval = arch_check_elf(&loc->elf_ex, !!interpreter, &arch_state);
if (retval)
goto out_free_dentry;
/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm);
if (retval)
goto out_free_dentry;
/* Do this immediately, since STACK_TOP as used in setup_arg_pages
may depend on the personality. */
SET_PERSONALITY2(loc->elf_ex, &arch_state);
if (elf_read_implies_exec(loc->elf_ex, executable_stack))
current->personality |= READ_IMPLIES_EXEC;
if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
current->flags |= PF_RANDOMIZE;
setup_new_exec(bprm);
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
if (retval < 0)
goto out_free_dentry;
current->mm->start_stack = bprm->p;
......
flush_old_exec( )를 통하여 process의 address space 변환이 이루어지고, thread group에 남아있는 thread들이 제거된다.
다음으로, task_struct의 personality를 판단한다.
그리고, setup_new_exec( ) 함수를 이용하여 방금 변환된 address space에 대한 초기화가 진행된다.
setup_arg_page( )와 randomize_stack_top( ) 함수를 통해서 stack 위치를 radom화 한다.
마지막으로 stack의 시작 위치를 mm_struct 구조체에 저장한다.
int flush_old_exec(struct linux_binprm * bprm)
{
int retval;
/*
* Make sure we have a private signal table and that
* we are unassociated from the previous thread group.
*/
retval = de_thread(current);
if (retval)
goto out;
/*
* Must be called _before_ exec_mmap() as bprm->mm is
* not visibile until then. This also enables the update
* to be lockless.
*/
set_mm_exe_file(bprm->mm, bprm->file);
/*
* Release all of the old mmap stuff
*/
acct_arg_size(bprm, 0);
retval = exec_mmap(bprm->mm);
if (retval)
goto out;
bprm->mm = NULL; /* We're using it now */
set_fs(USER_DS);
current->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_KTHREAD |
PF_NOFREEZE | PF_NO_SETAFFINITY);
flush_thread();
current->personality &= ~bprm->per_clear;
return 0;
out:
return retval;
}
EXPORT_SYMBOL(flush_old_exec);
flush_old_exec( ) 함수를 진입했다면, 새로운 process를 위한 address space 교환을 하기 전에 de_thread( )함수를 호출하여 현재 thread group 내의 모든 thread를 제거한다.
set_mm_exe_file( )함수를 통하여 process의 file path를 설정한다. 즉, mm_struct->exe_file을 설정한다.
다음으로, exec_mmap( ) 함수를 이용하여 새로운 process의 주소 공간을 bprm->mm에 저장되어 있는 주소공간으로 설정한다.
flush_thread( ) 함수를 호출하여 thread_struct의 Thread Local Storage의 data를 초기화 한다.
마지막으로, flags와 personality를 설정한다. personality는 구버전의 linux혹은 다른 OS와의 호환성을 위한 정보이다.
/*
* This function makes sure the current process has its own signal table,
* so that flush_signal_handlers can later reset the handlers without
* disturbing other processes. (Other processes might share the signal
* table via the CLONE_SIGHAND option to clone().)
*/
static int de_thread(struct task_struct *tsk)
{
struct signal_struct *sig = tsk->signal;
struct sighand_struct *oldsighand = tsk->sighand;
spinlock_t *lock = &oldsighand->siglock;
if (thread_group_empty(tsk))
goto no_thread_group;
/*
* Kill all other threads in the thread group.
*/
spin_lock_irq(lock);
if (signal_group_exit(sig)) {
/*
* Another group action in progress, just
* return so that the signal is processed.
*/
spin_unlock_irq(lock);
return -EAGAIN;
}
sig->group_exit_task = tsk;
sig->notify_count = zap_other_threads(tsk);
if (!thread_group_leader(tsk))
sig->notify_count--;
while (sig->notify_count) {
__set_current_state(TASK_KILLABLE);
spin_unlock_irq(lock);
schedule();
if (unlikely(__fatal_signal_pending(tsk)))
goto killed;
spin_lock_irq(lock);
}
spin_unlock_irq(lock);
/*
* At this point all other threads have exited, all we have to
* do is to wait for the thread group leader to become inactive,
* and to assume its PID:
*/
if (!thread_group_leader(tsk)) {
struct task_struct *leader = tsk->group_leader;
for (;;) {
threadgroup_change_begin(tsk);
write_lock_irq(&tasklist_lock);
/*
* Do this under tasklist_lock to ensure that
* exit_notify() can't miss ->group_exit_task
*/
sig->notify_count = -1;
if (likely(leader->exit_state))
break;
__set_current_state(TASK_KILLABLE);
write_unlock_irq(&tasklist_lock);
threadgroup_change_end(tsk);
schedule();
if (unlikely(__fatal_signal_pending(tsk)))
goto killed;
}
/*
* The only record we have of the real-time age of a
* process, regardless of execs it's done, is start_time.
* All the past CPU time is accumulated in signal_struct
* from sister threads now dead. But in this non-leader
* exec, nothing survives from the original leader thread,
* whose birth marks the true age of this process now.
* When we take on its identity by switching to its PID, we
* also take its birthdate (always earlier than our own).
*/
tsk->start_time = leader->start_time;
tsk->real_start_time = leader->real_start_time;
BUG_ON(!same_thread_group(leader, tsk));
BUG_ON(has_group_leader_pid(tsk));
/*
* An exec() starts a new thread group with the
* TGID of the previous thread group. Rehash the
* two threads with a switched PID, and release
* the former thread group leader:
*/
/* Become a process group leader with the old leader's pid.
* The old leader becomes a thread of the this thread group.
* Note: The old leader also uses this pid until release_task
* is called. Odd but simple and correct.
*/
tsk->pid = leader->pid;
change_pid(tsk, PIDTYPE_PID, task_pid(leader));
transfer_pid(leader, tsk, PIDTYPE_PGID);
transfer_pid(leader, tsk, PIDTYPE_SID);
list_replace_rcu(&leader->tasks, &tsk->tasks);
list_replace_init(&leader->sibling, &tsk->sibling);
tsk->group_leader = tsk;
leader->group_leader = tsk;
tsk->exit_signal = SIGCHLD;
leader->exit_signal = -1;
BUG_ON(leader->exit_state != EXIT_ZOMBIE);
leader->exit_state = EXIT_DEAD;
/*
* We are going to release_task()->ptrace_unlink() silently,
* the tracer can sleep in do_wait(). EXIT_DEAD guarantees
* the tracer wont't block again waiting for this thread.
*/
if (unlikely(leader->ptrace))
__wake_up_parent(leader, leader->parent);
write_unlock_irq(&tasklist_lock);
threadgroup_change_end(tsk);
release_task(leader);
}
sig->group_exit_task = NULL;
sig->notify_count = 0;
no_thread_group:
/* we have changed execution domain */
tsk->exit_signal = SIGCHLD;
exit_itimers(sig);
flush_itimer_signals();
if (atomic_read(&oldsighand->count) != 1) {
struct sighand_struct *newsighand;
/*
* This ->sighand is shared with the CLONE_SIGHAND
* but not CLONE_THREAD task, switch to the new one.
*/
newsighand = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
if (!newsighand)
return -ENOMEM;
atomic_set(&newsighand->count, 1);
memcpy(newsighand->action, oldsighand->action,
sizeof(newsighand->action));
write_lock_irq(&tasklist_lock);
spin_lock(&oldsighand->siglock);
rcu_assign_pointer(tsk->sighand, newsighand);
spin_unlock(&oldsighand->siglock);
write_unlock_irq(&tasklist_lock);
__cleanup_sighand(oldsighand);
}
BUG_ON(!thread_group_leader(tsk));
return 0;
killed:
/* protects against exit_notify() and __exit_signal() */
read_lock(&tasklist_lock);
sig->group_exit_task = NULL;
sig->notify_count = 0;
read_unlock(&tasklist_lock);
return -EAGAIN;
}
가장 먼저, thread_group_empty( ) 함수를 이용하여 thread group이 비어있는지 확인한다. 만약, 비어있다면 더이상 해당 함수를 실행할 필요가 없다.
다음으로, signal_group_exit( ) 함수를 통하여 thread group 삭제가 현재 진행중인지 확인한다. 만약, 이미 진행중 이라면 더이상 해당 함수를 실행할 필요가 없다.
zap_other_threads( ) 함수를 호출하여, 현재 thread를 제와한 group 내의 다른 thread에게 SIGKILL 시그널을 전송한다.
그리고, while문을 통하여 다른 thread들이 종료되기를 대기한다.
만약, 현재 thread가 group의 leader 라면, leader가 종료될때까지 대기한다.
본 함수의 나머지 부분은 현재 task가 leader가 아닌 경우에 현재 task를 leader로 만들기 위해 구현된 부분이다.
de_thread( )가 종료되면 다시 load_elf_binary( )로 돌아와서 새로운 process에 대한 sighand_struct의 초기화를 진행한다.
void setup_new_exec(struct linux_binprm * bprm)
{
arch_pick_mmap_layout(current->mm);
/* This is the point of no return */
current->sas_ss_sp = current->sas_ss_size = 0;
if (uid_eq(current_euid(), current_uid()) && gid_eq(current_egid(), current_gid()))
set_dumpable(current->mm, SUID_DUMP_USER);
else
set_dumpable(current->mm, suid_dumpable);
perf_event_exec();
__set_task_comm(current, kbasename(bprm->filename), true);
/* Set the new mm task size. We have to do that late because it may
* depend on TIF_32BIT which is only updated in flush_thread() on
* some architectures like powerpc
*/
current->mm->task_size = TASK_SIZE;
/* install the new credentials */
if (!uid_eq(bprm->cred->uid, current_euid()) ||
!gid_eq(bprm->cred->gid, current_egid())) {
current->pdeath_signal = 0;
} else {
would_dump(bprm, bprm->file);
if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP)
set_dumpable(current->mm, suid_dumpable);
}
/* An exec changes our domain. We are no longer part of the thread
group */
current->self_exec_id++;
flush_signal_handlers(current, 0);
do_close_on_exec(current->files);
}
EXPORT_SYMBOL(setup_new_exec);
setup_new_exec( ) 함수에 진입하기 전, 이미 새로운 프로세스를 위한 mm_struct로 교환했기 때문에, 이제 mm_struct에 대한 설정 및 초기화를 진행해야 한다.
arch_pick_mmap_layout( )함수를 통해서 mmap의 시작 지점을 설정한다.
다음으로, mm의 flag bit를 설정하고, kbasename( ) 함수를 이용해서 bprm->filename path의 가장 마지막 파일 이름을 얻어온다.
그리고, __set_task_comm( ) 함수를 통하여 process의 file path 설정 및 task_struct->comm 설정을 진행한다.
flush_signal_handler( ) 함수를 호출해서 signal handler를 모두 clear한다.
마지막으로, do_close_on_exec( ) 함수를 이용해서 file을 닫는다.
void arch_pick_mmap_layout(struct mm_struct *mm)
{
unsigned long random_factor = 0UL;
if (current->flags & PF_RANDOMIZE)
random_factor = arch_mmap_rnd();
if (mmap_is_legacy()) {
mm->mmap_base = TASK_UNMAPPED_BASE + random_factor;
mm->get_unmapped_area = arch_get_unmapped_area;
} else {
mm->mmap_base = mmap_base(random_factor);
mm->get_unmapped_area = arch_get_unmapped_area_topdown;
}
}
arch_mmap_rnd( )을 통하여 linearization range의 randomize 시작 주소를 얻는다. 해당 값은 "get_random_int( ) % (1<<28)" 이다.
mm->mmap_base에 TASK_UNMAPPED_BASE 값을 더하여 시작 주소를 randomize 한다.
마지막으로, 함수 포인터 mm->get_unmapped_area에 arch_ger_unmapped_area를 저장하여, virtual address를 할당하는데 사용한다.
/*
* Finalizes the stack vm_area_struct. The flags and permissions are updated,
* the stack is optionally relocated, and some extra space is added.
*/
int setup_arg_pages(struct linux_binprm *bprm,
unsigned long stack_top,
int executable_stack)
{
unsigned long ret;
unsigned long stack_shift;
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma = bprm->vma;
struct vm_area_struct *prev = NULL;
unsigned long vm_flags;
unsigned long stack_base;
unsigned long stack_size;
unsigned long stack_expand;
unsigned long rlim_stack;
#ifdef CONFIG_STACK_GROWSUP
/* Limit stack size */
stack_base = rlimit_max(RLIMIT_STACK);
if (stack_base > STACK_SIZE_MAX)
stack_base = STACK_SIZE_MAX;
/* Add space for stack randomization. */
stack_base += (STACK_RND_MASK << PAGE_SHIFT);
/* Make sure we didn't let the argument array grow too large. */
if (vma->vm_end - vma->vm_start > stack_base)
return -ENOMEM;
stack_base = PAGE_ALIGN(stack_top - stack_base);
stack_shift = vma->vm_start - stack_base;
mm->arg_start = bprm->p - stack_shift;
bprm->p = vma->vm_end - stack_shift;
#else
stack_top = arch_align_stack(stack_top);
stack_top = PAGE_ALIGN(stack_top);
if (unlikely(stack_top < mmap_min_addr) ||
unlikely(vma->vm_end - vma->vm_start >= stack_top - mmap_min_addr))
return -ENOMEM;
stack_shift = vma->vm_end - stack_top;
bprm->p -= stack_shift;
mm->arg_start = bprm->p;
#endif
if (bprm->loader)
bprm->loader -= stack_shift;
bprm->exec -= stack_shift;
down_write(&mm->mmap_sem);
vm_flags = VM_STACK_FLAGS;
/*
* Adjust stack execute permissions; explicitly enable for
* EXSTACK_ENABLE_X, disable for EXSTACK_DISABLE_X and leave alone
* (arch default) otherwise.
*/
if (unlikely(executable_stack == EXSTACK_ENABLE_X))
vm_flags |= VM_EXEC;
else if (executable_stack == EXSTACK_DISABLE_X)
vm_flags &= ~VM_EXEC;
vm_flags |= mm->def_flags;
vm_flags |= VM_STACK_INCOMPLETE_SETUP;
ret = mprotect_fixup(vma, &prev, vma->vm_start, vma->vm_end,
vm_flags);
if (ret)
goto out_unlock;
BUG_ON(prev != vma);
/* Move stack pages down in memory. */
if (stack_shift) {
ret = shift_arg_pages(vma, stack_shift);
if (ret)
goto out_unlock;
}
/* mprotect_fixup is overkill to remove the temporary stack flags */
vma->vm_flags &= ~VM_STACK_INCOMPLETE_SETUP;
stack_expand = 131072UL; /* randomly 32*4k (or 2*64k) pages */
stack_size = vma->vm_end - vma->vm_start;
/*
* Align this down to a page boundary as expand_stack
* will align it up.
*/
rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK;
#ifdef CONFIG_STACK_GROWSUP
if (stack_size + stack_expand > rlim_stack)
stack_base = vma->vm_start + rlim_stack;
else
stack_base = vma->vm_end + stack_expand;
#else
if (stack_size + stack_expand > rlim_stack)
stack_base = vma->vm_end - rlim_stack;
else
stack_base = vma->vm_start - stack_expand;
#endif
current->mm->start_stack = bprm->p;
ret = expand_stack(vma, stack_base);
if (ret)
ret = -EFAULT;
out_unlock:
up_write(&mm->mmap_sem);
return ret;
}
EXPORT_SYMBOL(setup_arg_pages);
setup_arg_pages( )의 인자 stack_top은 randomize되어 있다. 그러므로, stack_top에 대해서 page align을 진행한다.
그 다음, mm->arg_start = bprm->p - (vma_end - stack_top) 을 수행하여서 program의 시작위치를 잡아준다.
이제 남은 부분은 stack top에 맞추어서 flag bit를 설정하고, stack pointer를 설정한다. 즉, setup_arg_pages( ) 함수를 통해서 stack 맞추어 virtual address space를 수정한다.
마지막으로 ,expand_stack( ) 함수를 통해서 stack을 확장하는데 default stack size는 4개 page이다.
앞 부분에서 ELF Header를 이용한 초기화를 진행했고, Interpreter의 Segment Header를 로드하였다.
이제는 ELF의 PT_LOAD Segment에 헤앙하는 부분을 loading 한다.
static int load_elf_binary(struct linux_binprm *bprm)
{
......
/* Now we do a little grungy work by mmapping the ELF image into
the correct location in memory. */
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;
unsigned long total_size = 0;
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (unlikely (elf_brk > elf_bss)) {
unsigned long nbyte;
/* There was a PT_LOAD segment with p_memsz > p_filesz
before this one. Map anonymous pages, if needed,
and clear the area. */
retval = set_brk(elf_bss + load_bias,
elf_brk + load_bias);
if (retval)
goto out_free_dentry;
nbyte = ELF_PAGEOFFSET(elf_bss);
if (nbyte) {
nbyte = ELF_MIN_ALIGN - nbyte;
if (nbyte > elf_brk - elf_bss)
nbyte = elf_brk - elf_bss;
if (clear_user((void __user *)elf_bss +
load_bias, nbyte)) {
/*
* This bss-zeroing can fail if the ELF
* file specifies odd protections. So
* we don't check the return value
*/
}
}
}
if (elf_ppnt->p_flags & PF_R)
elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W)
elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X)
elf_prot |= PROT_EXEC;
elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
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_ET_DYN_BASE - vaddr;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
load_bias = ELF_PAGESTART(load_bias);
total_size = total_mapping_size(elf_phdata,
loc->elf_ex.e_phnum);
if (!total_size) {
retval = -EINVAL;
goto out_free_dentry;
}
}
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
if (BAD_ADDR(error)) {
retval = IS_ERR((void *)error) ?
PTR_ERR((void*)error) : -EINVAL;
goto out_free_dentry;
}
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;
/*
* Check to see if the section's size will overflow the
* allowed task size. Note that p_filesz must always be
* <= p_memsz so it is only necessary to check p_memsz.
*/
if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||
elf_ppnt->p_memsz > TASK_SIZE ||
TASK_SIZE - elf_ppnt->p_memsz < k) {
/* set_brk can never work. Avoid overflows. */
retval = -EINVAL;
goto out_free_dentry;
}
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;
}
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);
if (retval)
goto out_free_dentry;
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
retval = -EFAULT; /* Nobody gets to see this, but.. */
goto out_free_dentry;
}
......
먼저 program header를 순환하면서 PT_LOAD Header를 찾는다.
segment header의 flag값을 확인하여 elf_prot flag bit를 설정한다.
만약 File 종류가 ET_EXEC라면, 고정된 장소에 메모리를 할당하기 때문에 MAP_FIXED 플래그를 설정한다.
만약 File 종류가 ET_DYN이면은 ELF_ET_DYN_BASE로 부터 random 값을 더하여 메모리를 할당하여 주고, PF_RAMDOMIZE 플래그를 설정한다.
다음으로는, elf_map( ) 함수를 통하여 virtual address 공간에 mapping을 진행한다. 만약에 첫번째 mapping이라면 ELF file 내의 load_addr 정보가 필요하고, 만약 ET_DYN 유형일 경우에는 load_bias 정보가 필요한다.
주소가 낮음 곳에서 부터 text, data, bss, heap 영역이 차례로 위치하고, start_code는 위로, start_data는 아래로, elf_bss는 위로, elf_code는 위로, elf_data는 위로, elf_brk는 위로 증가한다.
그러므로 적절한 시작위치를 잡아준 후에, 시작위치에 load_bias를 더해준다.
마지막으로, set_brk( ) 함수를 통하여 elf_bss부터 elf_brk의 공간까지 매모리를 할당하여 준다.
static unsigned long elf_map(struct file *filep, unsigned long addr,
struct elf_phdr *eppnt, int prot, int type,
unsigned long total_size)
{
unsigned long map_addr;
unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr);
unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr);
addr = ELF_PAGESTART(addr);
size = ELF_PAGEALIGN(size);
/* mmap() will return -EINVAL if given a zero size, but a
* segment with zero filesize is perfectly valid */
if (!size)
return addr;
/*
* total_size is the size of the ELF (interpreter) image.
* The _first_ mmap needs to know the full size, otherwise
* randomization might put this image into an overlapping
* position with the ELF binary image. (since size < total_size)
* So we first map the 'big' image - and unmap the remainder at
* the end. (which unmap is needed for ELF images with holes.)
*/
if (total_size) {
total_size = ELF_PAGEALIGN(total_size);
map_addr = vm_mmap(filep, addr, total_size, prot, type, off);
if (!BAD_ADDR(map_addr))
vm_munmap(map_addr+size, total_size-size);
} else
map_addr = vm_mmap(filep, addr, size, prot, type, off);
return(map_addr);
}
인자로 전달되는 값들은,
filep : file 포인터
addr : mapping을 진행할 virtual address
size : file의 크기
off : file내에서의 offset
elf_map( ) 함수는 vm_mmap를 통해서 file에 표시되어 있는 만큼의 memory공간을 할당하고, 시작 주소를 반환한다.
바로 직전에는 ELF File내의 PT_LOAD 부분에 해당하는 segment에 대해서 memory mapping을 진행했다.
마찬가지로 interpreter에 대해서 elf_map( )을 통하여 mapping을 진행하고, process의 시작지점을 elf_entry에 저장한다.
static int load_elf_binary(struct linux_binprm *bprm)
{
......
if (elf_interpreter) {
unsigned long interp_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
if (!IS_ERR((void *)elf_entry)) {
/*
* load_elf_interp() returns relocation
* adjustment
*/
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
}
if (BAD_ADDR(elf_entry)) {
retval = IS_ERR((void *)elf_entry) ?
(int)elf_entry : -EINVAL;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
goto out_free_dentry;
}
}
kfree(interp_elf_phdata);
kfree(elf_phdata);
......
static int load_elf_binary(struct linux_binprm *bprm)
{
......
set_binfmt(&elf_format);
#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
retval = arch_setup_additional_pages(bprm, !!elf_interpreter);
if (retval < 0)
goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */
install_exec_creds(bprm);
retval = create_elf_tables(bprm, &loc->elf_ex,
load_addr, interp_load_addr);
if (retval < 0)
goto out;
/* N.B. passed_fileno might not be initialized? */
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
current->mm->brk = current->mm->start_brk =
arch_randomize_brk(current->mm);
#ifdef compat_brk_randomized
current->brk_randomized = 1;
#endif
}
if (current->personality & MMAP_PAGE_ZERO) {
/* Why this, you ask??? Well SVr4 maps page 0 as read-only,
and some applications "depend" upon this behavior.
Since we do not have the power to recompile these, we
emulate the SVr4 behavior. Sigh. */
error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
}
#ifdef ELF_PLAT_INIT
/*
* The ABI may specify that certain registers be set up in special
* ways (on i386 %edx is the address of a DT_FINI function, for
* example. In addition, it may also specify (eg, PowerPC64 ELF)
* that the e_entry field is the address of the function descriptor
* for the startup routine, rather than the address of the startup
* routine itself. This macro performs whatever initialization to
* the regs structure is required as well as any relocations to the
* function descriptor entries when executing dynamically links apps.
*/
ELF_PLAT_INIT(regs, reloc_func_desc);
#endif
start_thread(regs, elf_entry, bprm->p);
......
set_binfmt( ) 함수를 통해서 elf_format의 내용을 mm_struct->binfmt 변수에 저장한다.
install_exec_creds( )는 새로운 process의 cred구조체를 설정한다.
create_elf_tables( )는 interpreter와 process를 실행하기 전, user space의 stack과 heap에 추가적인 정보(시작 주소 등..)를 설정한다.
다음으로는 mm_struct에 code, data, bss, heap 위치를 저장하고 PF_RANDOMIZE 플래그에 따라서 위치를 다시한번 random화 한다.
마지막으로 start_thrad( ) 함수를 통하여 ELF를 실행한다.
#define start_thread(regs,pc,sp) \
({ \
memset(regs->uregs, 0, sizeof(regs->uregs)); \
if (current->personality & ADDR_LIMIT_32BIT) \
regs->ARM_cpsr = USR_MODE; \
else \
regs->ARM_cpsr = USR26_MODE; \
if (elf_hwcap & HWCAP_THUMB && pc & 1) \
regs->ARM_cpsr |= PSR_T_BIT; \
regs->ARM_cpsr |= PSR_ENDSTATE; \
regs->ARM_pc = pc & ~1; /* pc */ \
regs->ARM_sp = sp; /* sp */ \
nommu_start_thread(regs); \
})
#define nommu_start_thread(regs) regs->ARM_r10 = current->mm->start_data
pc는 interpreter 혹은 응용 프로그램의 code부분 시작주소를 말한다. sp는 user space에서 stack pointer를 말한다.
pc register를 새로운 시작주소로 jump 한다.
참고 및 출처
[ELF] ELF Header
ELF 란 용어를 많이 들어보셨을텐데요, ELF는 Executable and Linking Format의 약어입니다. UNIX / LINUX 기반에서 사용되는 실행 및 링킹 파일 포맷입니다. 이번 글에서는 ELF 파일 포맷에 대해 알아보겠습니
sonseungha.tistory.com
sys_execv源码分析_task_set_no_new_privs函数在哪里定义_二侠的博客-CSDN博客
因为最近工作涉及到linux的反汇编,做了一阵子Java搜索引擎的工作,再回到C很不适应,因此借着看源码回忆一点linux的知识,上一篇博文分析了_dl_runtime_resolve的源码,本章从头开始研究在linux下
blog.csdn.net
5.execve()到底干了啥?_chengonghao的博客-CSDN博客
导语 很多童鞋有分析阅读Linux源代码的强烈愿望,可是Linux内核代码量庞大,大部分人不知道如何下手,以下是我分析Linux源代码的一些经验,仅供参考,有不实之处请大神指正! 1.要想阅读内核
blog.csdn.net
Credentials in Linux — The Linux Kernel documentation
Credentials in Linux By: David Howells <dhowells@redhat.com> There are several parts to the security check performed by Linux when one object acts upon another: Objects. Objects are things in the system that may be acted upon directly by userspace programs
www.kernel.org
.
'Kernel > 理解' 카테고리의 다른 글
| Device Tree 문법 (0) | 2024.02.02 |
|---|---|
| ZRAM 분석 (0) | 2024.02.02 |
| Bootloader 개요 (0) | 2024.02.02 |
| [sched] Pressure Stall Information (PSI) (0) | 2024.02.02 |
| [mm][fs] File/Anon + VMA/Page (0) | 2024.02.02 |