程序装载过程

linux开机过程

1、开机自建,加载BIOS
2、读取MBR(Master Boot Record)主引导记录512字节;0柱面-磁头1扇区
3、Boot Loader grub引导菜单。
4、加载kernel内核
5、init进程依据initab文件来设定运行级别
6、init进程执行rc.sysinit
7、启动内核模块
8、执行不同运行级别的脚本程序
9、执行/etc/rc.d/rc.local
10、执行/bin/login程序,启动mingetty,进入登录状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yy7gs2xG-1618118049496)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615098940074.png)]

解释:
第一步:开机自检,加载BIOS

    当我们打开计算机电源的时候,随后会听到滴的一声,自检开始,这个过程中主要是检测我们的计算机硬件设备比如:CPU,内存,主板,显卡,CMOS等设备是否有故障存在

 

第二步:读取MBR

BIOS自检,首先会在一个Boot Sequence程序中搜索可以让系统启动的引导设备(比如我们有时在BIOS中设置为从硬盘启动,或者从CD-ROM启动等等)

     这时如果BIOS找不到可以引导的设备及相关程序后,便会启动失败,如果顺序的找到了相关设备硬盘,那么BIOS将把控制权交给启动设备中的MBR(Master Boot Record)主引导记录

MBR在大小为512字节,存放预启动信息、分区表等信息,

 

第三步:Boot Loader grub引导菜单

   在MBR程序中找到其前446字节的Boot Loader 

Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。

Boot Loader有若干种,其中Grub、Lilo和spfdisk是常见的Loader。。

系统读取内存中的grub配置信息(一般为menu.lst或grub.lst),并依照此配置信息来启动不同的操作系统。
第四步:加载kernel内核

根据grub设定的内核映像所在路径,系统读取内存映像,并进行解压缩操作。此时,屏幕一般会输出“Uncompressing Linux”的提示。当解压缩内核完成后,屏幕输出“OK, booting the kernel”。

 

系统将解压后的内核放置在内存之中,并调用start_kernel()函数来启动一系列的初始化函数并初始化各种设备,完成Linux核心环境的建立。至此,Linux内核已经建立起来了,基于Linux的程序应该可以正常运行了。

 

从全局启动历程start_kernel开始,

    内核完成的任务主要有:

    硬件的特测

    硬件驱动的初始化,

    挂载根文件系统(根切换)

    启动init进程。

    内核在系统启动后的功能先提前介绍一下:

    进程的调度,内存管理,文件系统的管理,硬件驱动,网络等

    内核自身初始化完成后开始下一步
第五步:init进程依据inittab文件夹来设定运行级别

内核被加载后,第一个运行的程序便是/sbin/init,该文件会读取/etc/inittab文件,并依据此文件来进行初始化工作。

 

其实/etc/inittab文件最主要的作用就是设定Linux的运行等级,其设定形式是“:id:5:initdefault:”,这就表明Linux需要运行在等级5上。Linux的运行等级设定如下:

0:-halt  关机

1:-single user mode 单用户模式

2:-Multi-user,without NFS无网络支持的多用户模式  类似于下面的run level3

3:-Full multi-user mode 有网络支持的多用户模式

4:-unused 保留,未使用

5:-X11  有网络支持有X-Window支持的多用户模式

6:- reboot 重新引导系统,即重启

 

cat /etc/inittab   查看/etc/inittab相关设定
第六步:init进程执行rc.sysinit

在设定了运行等级后,Linux系统执行的第一个用户层文件就是/etc/rc.d/rc.sysinit脚本程序,它做的工作非常多,包括设定PATH、设定网络配置(/etc/sysconfig/network)、启动swap分区、设定/proc等等。如果你有兴趣,可以到/etc/rc.d中查看一下rc.sysinit文件,里面的脚本够你看几天的
第七步:启动内核模块

具体是依据/etc/modules.conf文件或/etc/modules.d目录下的文件来装载内核模块。

 

第八步:执行不同运行级别的脚本程序

根据运行级别的不同,系统会运行rc0.d到rc6.d中的相应的脚本程序,来完成相应的初始化工作和启动相应的服务

 

 

第九步:执行/etc/rc.d/rc.local

你如果打开了此文件,里面有一句话,读过之后,你就会对此命令的作用一目了然:

 

# This script will be executed *after* all the other init scripts.

# You can put your own initialization stuff in here if you don’t

# want to do the full Sys V style init stuff.

 

rc.local就是在一切初始化工作后,Linux留给用户进行个性化的地方。你可以把你想设置和启动的东西放到这里。

 

第十步:执行/bin/login程序,启动mingetty,进入登录状态

进程源码分析

linux通过进程控制块PCB来对进程进行控制和管理
struct task_struct{
	volatile long state://进程状态
	void *stack;//进程堆栈指针
	atomic_t usage;
    unsigned int flags;
    unsigned int ptrace;
...
    //一些记录优先级的变量
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
    const struct sched_class *sched_class;
    struct sched_entity se;
    struct sched_rt_entity rt;
...
    //进程链表
    struct list_head tasks;
#ifdef CONFIG_SMP
    struct plist_node pushable_tasks;
    struct rb_node pushable_dl_tasks;
#endif
    //pid号
    pid_t pid;
    pid_t tgid;
...
    //父进程以及子进程
    struct task_struct __rcu *real_parent;
    struct task_struct __rcu *parent; 

    struct list_head children;    
    struct list_head sibling;
    struct task_struct *group_leader;
...
};
可以看到pcb记录了进程的ID号、优先级、状态、与其他进程关系等信息,操作系统由此对进程进行管理。
	需要注意的是,操作系统内核的具体实现中,是将当前的进程全部存入到一个循环链表中来进程管理的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FgfB4AcP-1618118049500)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615100309141.png)]

调试过程
gdb命令行
	b sys_clone
	b do_fork
	b dup_task_struct
	b copy_process
	b
	b copy_thread
	b ret_from_fork
此次我们在6个函数(or系统调用)处设置了断点
1、sys_clone;2.do_fork;3.dup_task_struct;4.copy_process;5.copy_thread;6.ret_from_fork;

实验中所使用的虚拟系统中,进程创建的底层函数是sys_clone,而sys_clone实际上调用的是do_fork,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yotrq7NR-1618118049502)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615103289552.png)]

目前系统找不到文件

只能pending,待定,悬而未决

do_fork的细节

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    ......

    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
    ......
}

在上个函数里,p是记录了新的进程的PCB变量,通过copy_process,系统将父进程的PCB拷贝并修改到子进程中,

copy_process函数则是首先利用dup_task_struct将父进程的PCB全盘拷贝,之后在具体修改与父进程不同的字部份

static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                     struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;

    //拷贝父进程的PCB
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    //修改具体的部分结构
    p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
    p->flags |= PF_FORKNOEXEC;
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    rcu_copy_process(p);
    p->vfork_done = NULL;
    spin_lock_init(&p->alloc_lock);
...
retval=copy_thread(clone_flags,stack_start,stack_size,p);
}

需要注意的是,在copy_process函数中有一个copy_thread函数,在它的函数实现中,将创建的子进程的栈底空间找到,并根据此修改了子进程的ip和sp数据:sp指向栈底,而ip指向ret_from_fork段,那ret_from_fork段又是什么呢,

ENTRY(ret_from_fork)
    CFI_STARTPROC
    pushl_cfi %eax
    call schedule_tail
    GET_THREAD_INFO(%ebp)
    popl_cfi %eax
    pushl_cfi $0x0202        # Reset kernel eflags
    popfl_cfi
    jmp syscall_exit
    CFI_ENDPROC
END(ret_from_fork)

可以看到,它最终跳转到了syscall_exit,而syscall_exit就是上次分析的中断处理程序sys_call中的代码,这样,sp指向的实际是我们触发终端后存储的上下文的那块堆栈,ip则指向syscall_exit,当新的进程创建时,它会首先执行syscall_exit处的代码。

总结:linux内核中,创建新的进程时,基本思路是赋值父进程的PCB给子进程,如果又不同的数据再进行调整,同时,将子进程执行的第一条命令指向中断恢复代码,从而实现创建新进程的效果。

程序装载过程

linux中主要的可执行文件为ELF文件,我们可以将它状态到自己的程序中,分析过程
首先明确一点,装载可执行程序又两种方式:静态链接与动态链接。所谓静态链接,就是在程序执行之前完成所有的链接工作,组成一个可执行文件,放到内存执行。这样做的缺点是:当有多个文件要链接同一份可执行文件时,内存中会有多份这个可执行文件的拷贝,这在一定程度上就是一种对内存的浪费,因此,人们又发明了动态链接的概念,它指的是程序执行前并不将所有的模块组装在一起,而是在需要用到这个模块的时候再完成链接工作,这样相比静态链接就更加灵活,也节省了内存,
动态链接分为装载时动态链接和运行时动态链接,大家可有兴趣可以进一步了解一下。

Linux装载可执行程序的系统调用是execve,它和fork函数一样,在执行过程中会更改执行完毕后返回的代码段

它的工作是首先读取传入的文件名,参数和环境变量,然后调用解析链表寻找解析该可执行文件的结构

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);

比如我们读入了elf文件,那它就要在链表中寻找elf文件的解析器,这里注意运用了观察者模式:linux会将各种解析器预先注册,当添加了新的解析器后,就会更改解析的链表,

比如elf文件中

static struct linux_bin_fmt elf_format={
	.module = THIS_MOUDLE,
	.load_binary=load_elf_binary,
	.load_shlib = load_elf_library,
	.core_dump = elf_core_dump,
	.min_coredump = ELF_EXEC_PAGESIZE,
};

在这里,它定义了load_lef_binary为解析器load_binary的具体实现(其实就是一种多态),之后将该结构体注册到解析器的链表中,从此再遇到elf文件,搜索解析器链表,就可以找到专门解析这种文件的解析器了。

static int __init init_elf_binfmt(void)
{
    register_binfmt(&elf_format);
    return 0;
}

在elf自己的解析函数load_elf_binary中,对于静态链接和动态链接,处理过程是不一样的

if (elf_interpreter) {
        unsigned long interp_map_addr = 0;
 
        elf_entry = load_elf_interp(&loc->interp_elf_ex,
                        interpreter,
                        &interp_map_addr,
                        load_bias);
        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;
        }
    }<br>...<br>start_thread(regs,elf_entry,bprm->p);

对于动态链接,这段代码直接执行elf_interpreter的部分,此时会装载一个动态链接器,由它再进行具体的内存管理,这里暂时不讨论

​ 对于静态链接,则直接执行else的部分,此时会将elf代码段的入口地址付给elf_entry变量

​ 之后会执行start_thread函数,该函数将进程上下文压栈,同时将elf_entry付给ip,对于静态链接来说,也就是使代码跳出内核态后执行的第一条代码就是elf入口处代码,

这样,就可以实现装载可执行程序的功能了,

​ 总结

linux装载可执行程序的过程,装载的可执行程序分为静态和动态链接两种方式。在解析可执行文件时,Linux利用了多态机制和观察者模式,并在解析过程中改变内核堆栈的eip地址,从而实现将执行的下一条代码更改到可执行程序的作用。

设置sys_execve、load_elf_binary、start_thread等系统调用作为断点进行调试,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vD4j5Sbf-1618118049504)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615108523515.png)]

传入elf_entry地址和我们链接的可执行文件hello的地址是一样的,这就说明,通过start_thread,系统将hello的程序入口地址设置为系统堆栈eip的指向,从而使用重新回到用户态之后首先执行hello

文件格式

本文讨论了 UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler and link editor output 汇编器和链接编辑器的输出)、COFF(Common Object File Format 通用对象文件格式)、ELF(Executable and Linking Format 可执行和链接格式)。首先是对可执行文件格式的一个综述,并通过描述 ELF 文件加载过程以揭示可执行文件内容与加载运行操作之间的关系。随后依此讨论了此三种文件格式,并着重讨论 ELF 文件的动态连接机制,其间也穿插了对各种文件格式优缺点的评价。最后对三种可执行文件格式有一个简单总结,并提出作者对可文件格式评价的一些感想。

可执行文件格式综述
相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。研究可执行文件的格式对编写高性能程序和一些黑客技术的运用都是非常有意义的。

不管何种可执行文件格式,一些基本的要素是必须的,显而易见的,文件中应包含代码和数据。因为文件可能引用外部文件定义的符号(变量和函数),因此重定位信息和符号信息也是需要的。一些辅助信息是可选的,如调试信息、硬件信息等。基本上任意一种可执行文件格式都是按区间保存上述信息,称为段(Segment)或节(Section)。不同的文件格式中段和节的含义可能有细微区别,但根据上下文关系可以很清楚的理解,这不是关键问题。最后,可执行文件通常都有一个文件头部以描述本文件的总体结构。

相对可执行文件有三个重要的概念:编译(compile)、连接(link,也可称为链接、联接)、加载(load)。源程序文件被编译成目标文件,多个目标文件被连接成一个最终的可执行文件,可执行文件被加载到内存中运行。因为本文重点是讨论可执行文件格式,因此加载过程也相对重点讨论。下面是LINUX平台下ELF文件加载过程的一个简单描述。

1:内核首先读ELF文件的头部,然后根据头部的数据指示分别读入各种数据结构,找到标记为可加载(loadable)的段,并调用函数 mmap()把段内容加载到内存中。在加载之前,内核把段的标记直接传递给 mmap(),段的标记指示该段在内存中是否可读、可写,可执行。显然,文本段是只读可执行,而数据段是可读可写。这种方式是利用了现代操作系统和处理器对内存的保护功能。著名的

Shellcode(参考资料 17)的编写技巧则是突破此保护功能的一个实际例子

2:内核分析出ELF文件标记为 PT_INTERP 的段中所对应的动态连接器名称,并加载动态连接器。现代 LINUX 系统的动态连接器通常是 /lib/ld-linux.so.2,相关细节在后面有详细描述。

3:内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。

4:内核把控制传递给动态连接器。

5:动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。

6:动态连接器对程序的外部引用进行重定位,通俗的讲,就是告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态连接还有一个延迟(Lazy)定位的特性,即只在"真正"需要引用符号时才重定位,这对提高程序运行效率有极大帮助。

7:动态连接器执行在ELF文件中标记为 .init 的节的代码,进行程序运行的初始化。在早期系统中,初始化代码对应函数 _init(void)(函数名强制固定),在现代系统中,则对应形式为

oid
__attribute((constructor))
init_function(void)
{
……
}

其中函数名为任意。

8:动态连接器把控制传递给程序,从 ELF 文件头部中定义的程序进入点开始执行。在 a.out 格式和ELF格式中,程序进入点的值是显式存在的,在 COFF 格式中则是由规范隐含定义。

从上面的描述可以看出,加载文件最重要的是完成两件事情:加载程序段和数据段到内存;进行外部定义符号的重定位。重定位是程序连接中一个重要概念。我们知道,一个可执行程序通常是由一个含有 main() 的主程序文件、若干目标文件、若干共享库(Shared Libraries)组成。(注:采用一些特别的技巧,也可编写没有 main 函数的程序,请参阅参考资料 2)一个 C 程序可能引用共享库定义的变量或函数,换句话说就是程序运行时必须知道这些变量/函数的地址。在静态连接中,程序所有需要使用的外部定义都完全包含在可执行程序中,而动态连接则只在可执行文件中设置相关外部定义的一些引用信息,真正的重定位是在程序运行之时。静态连接方式有两个大问题:如果库中变量或函数有任何变化都必须重新编译连接程序;如果多个程序引用同样的变量/函数,则此变量/函数会在文件/内存中出现多次,浪费硬盘/内存空间。比较两种连接方式生成的可执行文件的大小,可以看出有明显的区别。

a.out 文件格式分析
a.out 格式在不同的机器平台和不同的 UNIX 操作系统上有轻微的不同,例如在 MC680x0 平台上有 6 个 section。下面我们讨论的是最"标准"的格式。

a.out 文件包含 7 个 section,格式如下:
exec header(执行头部,也可理解为文件头部)
text segment(文本段)
data segment(数据段)
text relocations(文本重定位段)
data relocations(数据重定位段)
symbol table(符号表)
string table(字符串表)

执行头部的数据结构:

struct exec {
        unsigned long   a_midmag;    /* 魔数和其它信息 */
        unsigned long   a_text;      /* 文本段的长度 */
        unsigned long   a_data;      /* 数据段的长度 */
        unsigned long   a_bss;       /* BSS段的长度 */
        unsigned long   a_syms;      /* 符号表的长度 */
        unsigned long   a_entry;     /* 程序进入点 */
        unsigned long   a_trsize;    /* 文本重定位表的长度 */
        unsigned long   a_drsize;    /* 数据重定位表的长度 */
};

文件头部主要描述了各个 section 的长度,比较重要的字段是 a_entry(程序进入点),代表了系统在加载程序并初试化各种环境后开始执行程序代码的入口。这个字段在后面讨论的 ELF 文件头部中也有出现。由 a.out 格式和头部数据结构我们可以看出,a.out 的格式非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个 section 的顺序是固定的。这种结构缺乏扩展性,如不能包含"现代"可执行文件中常见的调试信息,最初的 UNIX 黑客对 a.out 文件调试使用的工具是 adb,而 adb 是一种机器语言调试器!

a.out 文件中包含符号表和两个重定位表,这三个表的内容在连接目标文件以生成可执行文件时起作用。在最终可执行的 a.out 文件中,这三个表的长度都为 0。a.out 文件在连接时就把所有外部定义包含在可执行程序中,如果从程序设计的角度来看,这是一种硬编码方式,或者可称为模块之间是强藕和的。在后面的讨论中,我们将会具体看到ELF格式和动态连接机制是如何对此进行改进的。

a.out 是早期UNIX系统使用的可执行文件格式,由 AT&T 设计,现在基本上已被 ELF 文件格式代替。a.out 的设计比较简单,但其设计思想明显的被后续的可执行文件格式所继承和发扬。可以参阅参考资料 16 和阅读参考资料 15 源代码加深对 a.out 格式的理解。参考资料 12 讨论了如何在"现代"的红帽LINUX运行 a.out 格式文件。

COFF 文件格式分析
COFF 格式比 a.out 格式要复杂一些,最重要的是包含一个节段表(section table),因此除了 .text,.data,和 .bss 区段以外,还可以包含其它的区段。另外也多了一个可选的头部,不同的操作系统可一对此头部做特定的定义。

COFF 文件格式如下:

File Header(文件头部)
Optional Header(可选文件头部)
Section 1 Header(节头部)
………
Section n Header(节头部)
Raw Data for Section 1(节数据)
Raw Data for Section n(节数据)
Relocation Info for Sect. 1(节重定位数据)
Relocation Info for Sect. n(节重定位数据)
Line Numbers for Sect. 1(节行号数据)
Line Numbers for Sect. n(节行号数据)
Symbol table(符号表)
String table(字符串表)
文件头部的数据结构:


struct filehdr
   {
     unsigned short  f_magic;    /* 魔数 */
       unsigned short  f_nscns;    /* 节个数 */
       long            f_timdat;   /* 文件建立时间 */
       long            f_symptr;   /* 符号表相对文件的偏移量 */
       long            f_nsyms;    /* 符号表条目个数 */
       unsigned short  f_opthdr;   /* 可选头部长度 */
       unsigned short  f_flags;    /* 标志 */
   };

COFF 文件头部中魔数与其它两种格式的意义不太一样,它是表示针对的机器类型,例如 0x014c 相对于 I386 平台,而 0x268 相对于 Motorola 68000系列等。当 COFF 文件为可执行文件时,字段 f_flags 的值为 F_EXEC(0X00002),同时也表示此文件没有未解析的符号,换句话说,也就是重定位在连接时就已经完成。由此也可以看出,原始的 COFF 格式不支持动态连接。为了解决这个问题以及增加一些新的特性,一些操作系统对 COFF 格式进行了扩展。Microsoft 设计了名为 PE(Portable Executable)的文件格式,主要扩展是在 COFF 文件头部之上增加了一些专用头部,具体细节请参阅参考资料 18,某些 UNIX 系统也对 COFF 格式进行了扩展,如 XCOFF(extended common object file format)格式,支持动态连接,请参阅参考资料 5。

紧接文件头部的是可选头部,COFF 文件格式规范中规定可选头部的长度可以为 0,但在 LINUX 系统下可选头部是必须存在的。下面是 LINUX 下可选头部的数据结构:

typedef struct 
{
    char   magic[2];    /* 魔数 */
    char   vstamp[2];    /* 版本号 */
    char   tsize[4];    /* 文本段长度 */
    char   dsize[4];    /* 已初始化数据段长度 */
    char   bsize[4];    /* 未初始化数据段长度 */
    char   entry[4];    /* 程序进入点 */
    char   text_start[4];       /* 文本段基地址 */
    char   data_start[4];       /* 数据段基地址 */
}
COFF_AOUTHDR;

字段 magic 为 0413 时表示 COFF 文件是可执行的,注意到可选头部中显式定义了程序进入点,标准的 COFF 文件没有明确的定义程序进入点的值,通常是从 .text 节开始执行,但这种设计并不好。

前面我们提到,COFF 格式比 a.out 格式多了一个节段表,一个节头条目描述一个节数据的细节,因此 COFF 格式能包含更多的节,或者说可以根据实际需要,增加特定的节,具体表现在 COFF 格式本身的定义以及稍早提及的 COFF 格式扩展。我个人认为,节段表的出现可能是 COFF 格式相对 a.out 格式最大的进步。下面我们将简单描述 COFF 文件中节的数据结构,因为节的意义更多体现在程序的编译和连接上,所以本文不对其做更多的描述。此外,ELF 格式和 COFF格式对节的定义非常相似,在随后的 ELF 格式分析中,我们将省略相关讨论。

struct COFF_scnhdr 
{
    char s_name[8];     /* 节名称 */
    char s_paddr[4];    /* 物理地址 */
   char s_vaddr[4];    /* 虚拟地址 */
    char s_size[4];     /* 节长度 */
   char s_scnptr[4];    /* 节数据相对文件的偏移量 */
    char s_relptr[4];    /* 节重定位信息偏移量 */
    char s_lnnoptr[4];    /* 节行信息偏移量 */
    char s_nreloc[2];    /* 节重定位条目数 */
    char s_nlnno[2];    /* 节行信息条目数 */
    char s_flags[4];    /* 段标记 */
};

有一点需要注意:LINUX系统中头文件coff.h中对字段 s_paddr的注释是"physical address",但似乎应该理解为"节被加载到内存中所占用的空间长度"。字段s_flags标记该节的类型,如文本段、数据段、BSS段等。在 COFF的节中也出现了行信息,行信息描述了二进制代码与源代码的行号之间的对映关系,在调试时很有用。

参考资料 19是一份对COFF格式详细描述的中文资料,更详细的内容请参阅参考资料 20。

ELF文件格式分析
ELF文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so。可执行文件:本文主要讨论的文件格式,总的来说,可执行文件的格式与上述两种文件的格式之间的区别主要在于观察的角度不同:一种称为连接视图(Linking View),一种称为执行视图(Execution View)。

首先看看ELF文件的总体布局:
ELF header(ELF头部)
Program header table(程序头表)
Segment1(段1)
Segment2(段2)
………
Sengmentn(段n)
Setion header table(节头表,可选)

段由若干个节(Section)构成,节头表对每一个节的信息有相关描述。对可执行程序而言,节头表是可选的。参考资料 1中作者谈到把节头表的所有数据全部设置为0,程序也能正确运行!ELF头部是一个关于本文件的路线图(road map),从总体上描述文件的结构。下面是ELF头部的数据结构:

typedef struct
{
    unsigned char e_ident[EI_NIDENT];     /* 魔数和相关信息 */
    Elf32_Half    e_type;                 /* 目标文件类型 */
    Elf32_Half    e_machine;              /* 硬件体系 */
    Elf32_Word    e_version;              /* 目标文件版本 */
    Elf32_Addr    e_entry;                /* 程序进入点 */
    Elf32_Off     e_phoff;                /* 程序头部偏移量 */
    Elf32_Off     e_shoff;                /* 节头部偏移量 */
    Elf32_Word    e_flags;                /* 处理器特定标志 */
    Elf32_Half    e_ehsize;               /* ELF头部长度 */
    Elf32_Half    e_phentsize;            /* 程序头部中一个条目的长度 */
    Elf32_Half    e_phnum;                /* 程序头部条目个数  */
    Elf32_Half    e_shentsize;            /* 节头部中一个条目的长度 */
    Elf32_Half    e_shnum;                /* 节头部条目个数 */
    Elf32_Half    e_shstrndx;             /* 节头部字符表索引 */
} Elf32_Ehdr;

下面我们对ELF头表中一些重要的字段作出相关说明,完整的ELF定义请参阅参考资料 6和参考资料7。

e_ident[0]-e_ident[3]包含了ELF文件的魔数,依次是0x7f、‘E’、‘L’、‘F’。注意,任何一个ELF文件必须包含此魔数。参考资料 3中讨论了利用程序、工具、/Proc文件系统等多种查看ELF魔数的方法。e_ident[4]表示硬件系统的位数,1代表32位,2代表64位。 e_ident[5]表示数据编码方式,1代表小印第安排序(最大有意义的字节占有最低的地址),2代表大印第安排序(最大有意义的字节占有最高的地址)。e_ident[6]指定ELF头部的版本,当前必须为1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式规范中定义这几个字节是被忽略的,但实际上是这几个字节完全可以可被利用。如病毒Lin/Glaurung.676/666(参考资料 1)设置e_ident[7]为0x21,表示本文件已被感染;或者存放可执行代码(参考资料 2)。ELF头部中大多数字段都是对子头部数据的描述,其意义相对比较简单。值得注意的是某些病毒可能修改字段e_entry(程序进入点)的值,以指向病毒代码,例如上面提到的病毒Lin/Glaurung.676/666。

一个实际可执行文件的文件头部形式如下:(利用命令readelf)

   ELF Header:
   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
   Class:                             ELF32
   Data:                              2's complement, little endian
   Version:                           1 (current)
   OS/ABI:                            UNIX - System V
   ABI Version:                       0
   Type:                              EXEC (Executable file)
   Machine:                           Intel 80386
   Version:                           0x1
   Entry point address:               0x80483cc
   Start of program headers:          52 (bytes into file)
   Start of section headers:          14936 (bytes into file)
   Flags:                             0x0
   Size of this header:               52 (bytes)
   Size of program headers:           32 (bytes)
   Number of program headers:         6
   Size of section headers:           40 (bytes)
   Number of section headers:         34
   Section header string table index: 31
   

紧接ELF头部的是程序头表,它是一个结构数组,包含了ELF头表中字段e_phnum定义的条目,结构描述一个段或其他系统准备执行该程序所需要的信息。
typedef struct {
      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;

在详细讨论可执行文件程序头表之前,首先查看一个实际文件的输出:
 Program Headers:
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
PHDR           0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP         0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
   LOAD           0x000000 0x08048000 0x08048000 0x00684 0x00684 R E 0x1000
   LOAD           0x000684 0x08049684 0x08049684 0x00118 0x00130 RW  0x1000
   DYNAMIC        0x000690 0x08049690 0x08049690 0x000c8 0x000c8 RW  0x4
   NOTE           0x000108 0x08048108 0x08048108 0x00020 0x00020 R   0x4

  Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt 
.init .plt .text .fini .rodata .eh_frame 
   03     .data .dynamic .ctors .dtors .jcr .got .bss 
   04     .dynamic 
05     .note.ABI-tag

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        080480f4 0000f4 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048108 000108 000020 00   A  0   0  4
  [ 3] .hash             HASH            08048128 000128 000040 04   A  4   0  4
  [ 4] .dynsym           DYNSYM          08048168 000168 0000b0 10   A  5   1  4
  [ 5] .dynstr           STRTAB          08048218 000218 00007b 00   A  0   0  1
  [ 6] .gnu.version      VERSYM          08048294 000294 000016 02   A  4   0  2
  [ 7] .gnu.version_r    VERNEED         080482ac 0002ac 000030 00   A  5   1  4
  [ 8] .rel.dyn          REL             080482dc 0002dc 000008 08   A  4   0  4
  [ 9] .rel.plt          REL             080482e4 0002e4 000040 08   A  4   b  4
  [10] .init             PROGBITS        08048324 000324 000017 00  AX  0   0  4
  [11] .plt              PROGBITS        0804833c 00033c 000090 04  AX  0   0  4
  [12] .text             PROGBITS        080483cc 0003cc 0001f8 00  AX  0   0  4
  [13] .fini             PROGBITS        080485c4 0005c4 00001b 00  AX  0   0  4
  [14] .rodata           PROGBITS        080485e0 0005e0 00009f 00   A  0   0 32
  [15] .eh_frame         PROGBITS        08048680 000680 000004 00   A  0   0  4
  [16] .data             PROGBITS        08049684 000684 00000c 00  WA  0   0  4
  [17] .dynamic          DYNAMIC         08049690 000690 0000c8 08  WA  5   0  4
  [18] .ctors            PROGBITS        08049758 000758 000008 00  WA  0   0  4
  [19] .dtors            PROGBITS        08049760 000760 000008 00  WA  0   0  4
  [20] .jcr              PROGBITS        08049768 000768 000004 00  WA  0   0  4
  [21] .got              PROGBITS        0804976c 00076c 000030 04  WA  0   0  4
  [22] .bss              NOBITS          0804979c 00079c 000018 00  WA  0   0  4
  [23] .comment          PROGBITS        00000000 00079c 000132 00      0   0  1
  [24] .debug_aranges    PROGBITS        00000000 0008d0 000098 00      0   0  8
  [25] .debug_pubnames   PROGBITS        00000000 000968 000040 00      0   0  1
  [26] .debug_info       PROGBITS        00000000 0009a8 001cc6 00      0   0  1
  [27] .debug_abbrev     PROGBITS        00000000 00266e 0002cc 00      0   0  1
  [28] .debug_line       PROGBITS        00000000 00293a 0003dc 00      0   0  1
  [29] .debug_frame      PROGBITS        00000000 002d18 000048 00      0   0  4
  [30] .debug_str        PROGBITS        00000000 002d60 000bcd 01  MS  0   0  1
  [31] .shstrtab         STRTAB          00000000 00392d 00012b 00      0   0  1
  [32] .symtab           SYMTAB          00000000 003fa8 000740 10     33  56  4
  [33] .strtab           STRTAB          00000000 0046e8 000467 00      0   0  1
  
  对一个ELF可执行程序而言,一个基本的段是标记p_type为PT_INTERP的段,它表明了运行此程序所需要的程序解释器(/lib/ld- linux.so.2),实际上也就是动态连接器(dynamic linker)。最重要的段是标记p_type为PT_LOAD的段,它表明了为运行程序而需要加载到内存的数据。查看上面实际输入,可以看见有两个可 LOAD段,第一个为只读可执行(FLg为R E),第二个为可读可写(Flg为RW)。段1包含了文本节.text,注意到ELF文件头部中程序进入点的值为0x80483cc,正好是指向节. text在内存中的地址。段二包含了数据节.data,此数据节中数据是可读可写的,相对的只读数据节.rodata包含在段1中。ELF格式可以比 COFF格式包含更多的调试信息,如上面所列出的形式为.debug_xxx的节。在I386平台LINUX系统下,用命令file查看一个ELF可执行程序的可能输出是:a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped。

他文章里还有一段话:
不同时期的可执行文件格式深刻的反映了技术进步的过程,技术进步通常是针对解决存在的问题和适应新的环境。早期的UNIX系统使用a.out格式,随着操作系统和硬件系统的进步,a.out格式的局限性越来越明显。新的可执行文件格式COFF在UNIX System VR3中出现,COFF格式相对a.out格式最大变化是多了一个节头表(section head table),能够在包含基础的文本段、数据段、BSS段之外包含更多的段,但是COFF对动态连接和C++程序的支持仍然比较困难。为了解决上述问题, UNIX系统实验室(UNIX SYSTEM Laboratories USL) 开发出ELF文件格式,它被作为应用程序二进制接口(Application binary Interface ABI)的一部分,其目的是替代传统的a.out格式。例如,ELF文件格式中引入初始化段.init和结束段.fini(分别对应构造函数和析构函数)则主要是为了支持C++程序。1994年6月ELF格式出现在LINUX系统上,现在ELF格式作为UNIX/LINUX最主要的可执行文件格式。当然我们完全有理由相信,在将来还会有新的可执行文件格式出现。

所以我就更觉得elf是一种更新得更效率得替代a.out技术吧
至于gcc/g++生成得a.out是不是elf

我按照图片得样子查看了个a.out,完全符合elf得数据结构

readelf -h a.out;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9owey1wd-1618118049506)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615112386877.png)]

esp和ebp的变化情况

ebp(extended base pointer)寄存器存放当前栈帧的栈底,一般在函数内不会对ebp寄存器做变动;esp(extended stack pointer)寄存器存放当前栈帧的栈顶,会随着当前函数内栈空间的开辟而变动
首先明确:push pop就是对esp的操作
因为栈是从高地址向低地址增长的,所以push是对esp进行减法,然后将操作数写到上述寄存器里的指针所指向的内存中
pop是对esp进行加法:它先从栈指针指向的内存中读取数据,用以备用(通常是写到其他寄存器里),然后再将栈指针的数值加上4或8
比如:push %eax前esp=0x115fc68那么push后esp=0x115fc64:原来的值减4

void test(){
	int a = 0;
}
int main(){
	int a = 0;
	test();
}

在上面这段代码中,ebp和esp的变动是这样的

首先进入main函数中,
push %ebp,保存上一个函数即_start函数的栈底。
此时esp就指向保存ebp的栈顶
然后将esp赋值给ebp作为当前函数新的栈底,此时ebp和esp指向同一个地址,
此时执行a=4;那么esp减去4,同时将0赋值给esp指向的地址。
如果调用test时有参数要传递,那么参数是保存在调用者,这里是main函数的栈帧中的。返回地址也是保存在调用者main函数的栈帧中,压栈顺序是这样:先将最后一个参数入栈,然后到数第二个,到数第三个。。参数压栈完毕后将返回地址压栈,返回地址也指示了main函数栈帧的结束处
	进入test函数后,一样的,要压入ebp,将esp赋值给ebp,而且分配内存给a,
	执行完后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idX8PrYT-1618118049508)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615114034777.png)]

执行完test函数后,函数返回。恢复main函数的栈底和栈定,即将esp和ebp恢复,函数返回后内存布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N4JjW1Na-1618118049509)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1615114120908.png)]

自然,在test函数内部的a被随后的执行所覆盖

从栈指针指向的内存中读取数据,用以备用(通常是写到其他寄存器里),然后再将栈指针的数值加上4或8
比如:push %eax前esp=0x115fc68那么push后esp=0x115fc64:原来的值减4


void test(){
int a = 0;
}
int main(){
int a = 0;
test();
}


在上面这段代码中,ebp和esp的变动是这样的

首先进入main函数中,
push %ebp,保存上一个函数即_start函数的栈底。
此时esp就指向保存ebp的栈顶
然后将esp赋值给ebp作为当前函数新的栈底,此时ebp和esp指向同一个地址,
此时执行a=4;那么esp减去4,同时将0赋值给esp指向的地址。
如果调用test时有参数要传递,那么参数是保存在调用者,这里是main函数的栈帧中的。返回地址也是保存在调用者main函数的栈帧中,压栈顺序是这样:先将最后一个参数入栈,然后到数第二个,到数第三个。。参数压栈完毕后将返回地址压栈,返回地址也指示了main函数栈帧的结束处
进入test函数后,一样的,要压入ebp,将esp赋值给ebp,而且分配内存给a,
执行完后


[外链图片转存中...(img-idX8PrYT-1618118049508)]

执行完test函数后,函数返回。恢复main函数的栈底和栈定,即将esp和ebp恢复,函数返回后内存布局

[外链图片转存中...(img-N4JjW1Na-1618118049509)]

自然,在test函数内部的a被随后的执行所覆盖

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值