学习Linux 内核如何装载和启动一个可执行程序

ELF文件格式

概述

目标文件是编译器生成的文件,"目标"指目标平台,常见的有x86,arm等,决定编译器使用机器指令集,手机就常用arm架构。目标文件一般叫ABI(应用程序二进制接口),目标文件和目标平台应该兼容比如x86汇编只能在x86架构平台下执行,不能在arm架构平台下执行。
ELF是可执行,可链接的格式。是Linux的常用格式。(Windows常用形式是PE)
在这里插入图片描述
比如在Linux系统中绿色文件的一般就是可执行文件,使用file命令可查看这个文件,发现是ELF格式。ELF是一种对象文件格式,用于定义不同类型对象文件都有什么内容,以什么样格式放这些内容。

ELF文件三种类型

1.可重定位文件:这种文件为中间文件,还需继续处理,比如.o文件,需要链接才可生成一个可执行文件
在这里插入图片描述
在这里插入图片描述
2.可执行文件:由多个可重定位文件结合而成
3. 动态库文件(.so)
在这里插入图片描述

ELF文件结构

ELF文件主体是各种节,.text(代码节),.section,.data(数据节)以及描述这些节属性的信息和ELF文件整体描述信息。
在这里插入图片描述
这里的.rodata存放C中的字符串和#define定义常量。
ELF header在文件最开始描述了文件的组织情况。比如是32位还是64位。再比如ELF文件的属性信息,是可执行文件还是可重定位文件还是动态库文件。
Section Header描述了文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称,节区大小等信息,用于链接的目标文件必须包含节区头部表,其他目标文件没有这个表是可以的。
Program Head表与创建进程相关,描述节在文件中位置,大小以及放进内存后位置和大小。可重定位文件不用这个表

编程使用 exec*库函数加载一个可执行文件

Linux中有exec函数,里面包含execl,execlp,execle,execv,execvp和execve六个函数,这六个函数差异在于对命令行参数和环境变量参数传递方式不同
在这里插入图片描述
随便查看一个execve(man execve)可看到函数描述
在这里插入图片描述

小demo-尝试execve的功能(exec族)

来做一个demo,验证execve的功能,写两个函数,第一个是myecho.c,第二个是myexecve.c,功能是打印出命令行,即打印出argv中的参数(从argv[1]开始)

代码1(myecho.c):

//打印命令行参数
#include<stdio.h>
#include<unistd.h>

#include<stdlib.h>

int main(int argc,char*argv[]){
        for(int i=0;i<argc;i++){
                printf("argv[%d]:%s\n",i,argv[i]);
        }
        exit(EXIT_SUCCESS);
}                                                                                        
代码段2: myexecve.c

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(int argc,char* argv[]){
        //这里的结尾必须加NULL 否则调用execve会报Bad address错误-血的教训
        char*list1[]={NULL,"Hello","Linux","Boris",NULL};
        char*list2[]={NULL};
        //参数数量必须为2 不为2报错
        if(argc!=2){
                fprintf(stderr,"%s wrong",argv[0]);
                exit(EXIT_FAILURE);
        }
        //将list1第一个参数放为刚刚的那个echo文件的文件名
        list1[0]=argv[1];
        //execve
        execve(argv[1],list1,list2);
        perror("execve");
}

execve函数执行argv[1]这个可执行文件
在这里插入图片描述

可执行程序运行时动态链接

首先创建一个.h文件,代码如下

#ifndef __LIBSHARED_TEST_H_
#define __LIBSHARED_TEST_H_

int testAPI();
#endif         

特别简单,里面就一个测试函数。
然后写一个.c文件,代码如下:

#include<stdio.h>
#include"libshared_test.h"

int testAPI(){
        printf("Hello Linux and Boris\n");
}

接下来将这个.c文件编译为.so文件(动态库文件编译需要加-shared)

gcc -shared libshared_test.c -o libshared_test.so

在这里插入图片描述

写入main1文件,然后测试动态库

#include<stdio.h>
#include"libshared_test.h"

int main(){
        printf("测试运行程序时动态链接\n");
        testAPI();
}

在这里插入图片描述
这里的-I 后面接的是头文件所在目录, -L接的是库文件所在目录, -l 后面接的是库文件名,别把lib加进去

可执行程序装载时动态链接

将那个libshared_test.c文件编译为libshared_test1.so用于后面可执行程序装载时动态链接

gcc -shared libshared_test.c -o libshared_test1.so

编写main.c代码

#include<stdio.h>
#include"libshared_test.h"
//可对库文件进行操作
#include<dlfcn.h>

int main(){
	printf("可执行程序装载动态链接\n");
	//打开库文件libshared_test1.so
	void *handle=dlopen("libshared_test1.so",RTLD_NOW);
	if(handle==NULL){
		printf("打开动态加载库文件错误:%s\n",dlerror());
		return -1;
	}
	//函数指针
	int(*func1)(void);
	char* info_error;
	//去这个so文件中找这个函数
	func1=dlsym(handle,"testAPI");
	//有报错信息
	if((info_error=dlerror())!=NULL){
		printf("testAPI函数没找到\n");
		return -1;
	}
	printf("调用装载库函数\n");
	func1();
	dlclose(handle);
	return 0;
}

然后进行编译
在这里插入图片描述
这里加入libdl.so的动态库是因为头文件有dlfcn.h, 不加这个动态库会报错,第二行那个export LD_LIBRARY_PATH是环境变量,如果能通过编译却执行报错就必须加这一行。

使用gdb调试

首先下载menu

git clone https://github.com/mengning/menu.git

然后将test_exec.c复制为test.c

sudo mv test_exec.c test.c

让我们来看一看test_exec.c
在这里插入图片描述
在这里插入图片描述
execlp会执行./hello
来看看Makefile
在这里插入图片描述
用到了多线程编译,具体的应该不难 cpio是打包
使用sudo make rootfs命令启动
在这里插入图片描述
在这里插入图片描述
现在使用gdb进行调试,使用如下命令使qemu停止

cd ..
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

再打开另一窗口

cd /home/boris
cd linux-3.18.6
gdb
file vmlinux
target remote:1234

在file vmlinux即加载内核操作时要有done字样
实际exec族函数都是使用**sys_execve()**这个系统调用来执行一个ELF文件,所以在sys_execve()设置断点
在这里插入图片描述
在这里插入图片描述
调用关系如下
在这里插入图片描述
sys_execve的代码详见代码

execve详解

execve在执行时就陷入内核态,execve加载的程序把正在执行进程覆盖掉,系统调用返回时就返回新的可执行程序起点。
execve系统调用实质是运行内核态下的sys_execve()函数,处理过程如下:

  1. sys_execve下的do_execve读取128字节文件头部,由此判断可执行文件类型
  2. 调用search_binary_handler去搜索与匹配合适的可执行文件装载处理过程
  3. ELF文件由load_elf_binary函数负责装载,load_elf_binary函数调用start_thread,创建新进程堆栈,有pt_regs栈底指针,重要的是修改了中断现场中保存的EIP寄存器
    对于静态链接:elf_entry指向可执行文件头部,一般为main函数,为新程序执行起点,新可执行程序起点一般地址为0x804800位置,由编译器定,可能由于安全考虑不固定。
    对于动态链接:elf_entry指向动态链接器(ld)起点load_elf_interp

总结

这次与上次实验学了fork和execve函数调用,可以看出其区别,fork有两次返回,一次是返回父进程继续往下执行,一次是子进程返回到ret_from_fork然后正常回到用户态,而execve是先进入内核态,执行结束就回到用户态,从这点来说fork更特殊。这次加深了对exec族的理解和使用可谓收益匪浅,如果出现了错误,请各位大佬指正,因为这也是我的一个学习过程,对很多还是未知。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值