Linux0.11内核--加载二进制文件之1.copy_strings

从现在开始就是分析最后的核心模块exec.c了,分析完这个文件后,就会和之前的所有分析形成一个环路,从创建进程、加载进程程序到进程调度、内存管理。

exec.c的核心do_execve函数很长,而且用到了很多其他的函数,copy_strings就是其中一个,我们这里就先来分析这个函数。

首先看调用处,在main.c中:

static char *argv_rc[] =
{
"/bin/sh", NULL};		// 调用执行程序时参数的字符串数组。
static char *envp_rc[] =
{
"HOME=/", NULL};		// 调用执行程序时的环境字符串数组。

void init(void){
...
    execve ("/bin/sh", argv_rc, envp_rc);	// 替换成/bin/sh 程序并执行。
...
}

再看exec.c中:

/*
* MAX_ARG_PAGES 定义了新程序分配给参数和环境变量使用的内存最大页数。
* 32 页内存应该足够了,这使得环境和参数(env+arg)空间的总合达到128kB!
*/
#define MAX_ARG_PAGES 32

do_execve (unsigned long *eip, long tmp, char *filename,
	   char **argv, char **envp)
{
    unsigned long page[MAX_ARG_PAGES];	// 参数和环境字符串空间的页面指针数组。
    int i, argc, envc;
    // 参数和环境字符串空间中的偏移指针,初始化为指向该空间的最后一个长字处。
  unsigned long p = PAGE_SIZE * MAX_ARG_PAGES - 4;
...
    // 计算参数个数和环境变量个数。
  argc = count (argv);
  envc = count (envp);

    // 若sh_bang 标志没有设置,则设置它,并复制指定个数的环境变量串和参数串到参数和环境空间中。
      if (sh_bang++ == 0)
	{
	  p = copy_strings (envc, envp, page, p, 0);
	  p = copy_strings (--argc, argv + 1, page, p, 0);
	}
...
}

mm.h:

#define PAGE_SIZE 4096		// 定义内存页面的大小(字节数)。

exec.c和segment.h放在一起:

/*
* count()函数计算命令行参数/环境变量的个数。
*/
//// 计算参数个数。
// 参数:argv - 参数指针数组,最后一个指针项是NULL。
// 返回:参数个数。
static int
count (char **argv)
{
  int i = 0;
  char **tmp;

  if (tmp = argv)
    while (get_fs_long ((unsigned long *) (tmp++)))
      i++;

  return i;
}

//// 读取fs 段中指定地址处的长字(4 字节)。
// 参数:addr - 指定的内存地址。
// %0 - (返回的长字_v);%1 - (内存地址addr)。
// 返回:返回内存fs:[addr]处的长字。
extern inline unsigned long
get_fs_long (const unsigned long *addr)
{
  unsigned long _v;

__asm__ ("movl %%fs:%1,%0": "=r" (_v):"m" (*addr));
  return _v;
}

先分析获取参数/环境变量的个数,首先声明了两个指针数组argv_rc和envp_rc并传入execve。

int* a[4]     指针数组

表示:数组a中的元素都为int型指针

注意do_execve的形参为char **argv, char **envp,指针的指针。所以也就是说在count函数中,tmp++是指针数组argv_rc的其中的元素的地址,那么在get_fs_long中*addr指的是argv_rc的元素的值(也就是"/bin/sh"这个char类型指针),因为使用的是fs:%1而不是fs:[%1],因此最终_v得到的是char类型的完整地址。所以count就是根据是不是有地址值来判断数量。

/*
* ‘copy_string()‘函数从用户内存空间拷贝参数和环境字符串到内核空闲页面内存中。
* 这些已具有直接放到新用户内存中的格式。
*
* 由TYT(Tytso)于1991.12.24 日修改,增加了from_kmem 参数,该参数指明了字符串或
* 字符串数组是来自用户段还是内核段。
*
* from_kmem        argv *      argv **
*          0                用户空间      用户空间
*          1                 内核空间      用户空间
*          2                 内核空间      内核空间
*
* 我们是通过巧妙处理fs 段寄存器来操作的。由于加载一个段寄存器代价太大,所以
* 我们尽量避免调用set_fs(),除非实在必要。
*/
//// 复制指定个数的参数字符串到参数和环境空间。
// 参数:argc - 欲添加的参数个数;argv - 参数指针数组;page - 参数和环境空间页面指针数组。
// p -在参数表空间中的偏移指针,始终指向已复制串的头部;from_kmem - 字符串来源标志。
// 在do_execve()函数中,p 初始化为指向参数表(128kB)空间的最后一个长字处,参数字符串
// 是以堆栈操作方式逆向往其中复制存放的,因此p 指针会始终指向参数字符串的头部。
// 返回:参数和环境空间当前头部指针。
static unsigned long
copy_strings (int argc, char **argv, unsigned long *page,
	      unsigned long p, int from_kmem)
{
  char *tmp, *pag;
  int len, offset = 0;
  unsigned long old_fs, new_fs;

  if (!p)
    return 0;			/* bullet-proofing *//* 偏移指针验证 */
// 取ds 寄存器值到new_fs,并保存原fs 寄存器值到old_fs。
  new_fs = get_ds ();
  old_fs = get_fs ();
// 如果字符串和字符串数组来自内核空间,则设置fs 段寄存器指向内核数据段(ds)。
  if (from_kmem == 2)
    set_fs (new_fs);
// 循环处理各个参数,从最后一个参数逆向开始复制,复制到指定偏移地址处。
  while (argc-- > 0)
    {
// 如果字符串在用户空间而字符串数组在内核空间,则设置fs 段寄存器指向内核数据段(ds)。
      if (from_kmem == 1)
	set_fs (new_fs);
// 从最后一个参数开始逆向操作,取fs 段中最后一参数指针到tmp,如果为空,则出错死机。
      if (!(tmp = (char *) get_fs_long (((unsigned long *) argv) + argc)))
	panic ("argc is wrong");
// 如果字符串在用户空间而字符串数组在内核空间,则恢复fs 段寄存器原值。
      if (from_kmem == 1)
	set_fs (old_fs);
// 计算该参数字符串长度len,并使tmp 指向该参数字符串末端。
      len = 0;			/* remember zero-padding */
      do
	{			/* 我们知道串是以NULL 字节结尾的 */
	  len++;
	}
      while (get_fs_byte (tmp++));
// 如果该字符串长度超过此时参数和环境空间中还剩余的空闲长度,则恢复fs 段寄存器并返回0。
      if (p - len < 0)
	{			/* this shouldn‘t happen - 128kB */
	  set_fs (old_fs);	/* 不会发生-因为有128kB 的空间 */
	  return 0;
	}
// 复制fs 段中当前指定的参数字符串,是从该字符串尾逆向开始复制。
      while (len)
	{
	  --p;
	  --tmp;
	  --len;
// 函数刚开始执行时,偏移变量offset 被初始化为0,因此若offset-1<0,说明是首次复制字符串,
// 则令其等于p 指针在页面内的偏移值,并申请空闲页面。
	  if (--offset < 0)
	    {
	      offset = p % PAGE_SIZE;
// 如果字符串和字符串数组在内核空间,则恢复fs 段寄存器原值。
	      if (from_kmem == 2)
		set_fs (old_fs);
// 如果当前偏移值p 所在的串空间页面指针数组项page[p/PAGE_SIZE]==0,表示相应页面还不存在,
// 则需申请新的内存空闲页面,将该页面指针填入指针数组,并且也使pag 指向该新页面,若申请不
// 到空闲页面则返回0。
	      if (!(pag = (char *) page[p / PAGE_SIZE]) &&
		  !(pag = (char *) page[p / PAGE_SIZE] =
		    (unsigned long *) get_free_page ()))
		return 0;
// 如果字符串和字符串数组来自内核空间,则设置fs 段寄存器指向内核数据段(ds)。
	      if (from_kmem == 2)
		set_fs (new_fs);

	    }
// 从fs 段中复制参数字符串中一字节到pag+offset 处。
	  *(pag + offset) = get_fs_byte (tmp);
	}
    }
// 如果字符串和字符串数组在内核空间,则恢复fs 段寄存器原值。
  if (from_kmem == 2)
    set_fs (old_fs);
// 最后,返回参数和环境空间中已复制参数信息的头部偏移值。
  return p;
}

首先p是指向参数和环境空间的最后一个长字处,逻辑地址,如下图所示

首先从最后一个参数开始逆向操作,取fs段中最后一个参数指针到tmp。

然后取字符串长度,注意get_fs_byte的*addr为字符指针指向的值,也就是_v得到的是字符值一个字节。

最后是从字符串尾部开始逆向复制,注意page数组不是用来映射的,而是保存内存页的地址。而offset是每次循环都会变化。

最终返回p。

时间: 2024-10-10 10:29:56

Linux0.11内核--加载二进制文件之1.copy_strings的相关文章

Linux0.11内核--加载可执行二进制文件之3.exec

最后剩下最核心的函数do_execve了,由于这里为了简单起见我不分析shell命令的情况, /* * 'do_execve()'函数执行一个新程序. */ //// execve()系统中断调用函数.加载并执行子进程(其它程序). // 该函数系统中断调用(int 0x80)功能号__NR_execve 调用的函数. // 参数:eip - 指向堆栈中调用系统中断的程序代码指针eip 处,参见kernel/system_call.s 程序 // 开始部分的说明:tmp - 系统中断调用本函数时

Linux0.11内核--加载可执行二进制文件之2.change_ldt

前面分析完了copy_strings函数,这里来分析另一个注意的函数change_ldt. 先来看调用处: // 根据a_text 修改局部表中描述符基址和段限长,并将参数和环境空间页面放置在数据段末端. // 执行下面语句之后,p 此时是以数据段起始处为原点的偏移值,仍指向参数和环境空间数据开始处, // 也即转换成为堆栈的指针. p += change_ldt (ex.a_text, page) - MAX_ARG_PAGES * PAGE_SIZE; 解释的很清楚,也就是说p指向的是相当于

linux0.11内核fork实现分析(不看不知道,一看很简单)

pcDuino3下支持mmc启动,官方的Uboot是采用SPL框架实现的,因为内部的SRAM空间达到32K,我们完全可以在这32K空间内编写一个完整可用小巧的bootloader来完成引导Linux kernel的目的. 我们首先介绍下SPL框架,可以先看下<GNU ARM汇编--(十八)u-boot-采用nand_spl方式的启动方法>和<GNU ARM汇编--(十九)u-boot-nand-spl启动过程分析>,NAND_SPL也算是SPL框架下的一种模式. 当使用Nand f

Linux-0.11内核源码分析系列:关于线性地址,逻辑地址,物理地址的关系与区别

/* *Author : DavidLin *Date : 2014-11-22pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-11-22 created this file! * 2) */     以下所有描述基于Linux0.11内核及其所编写的年

Linux0.11内核剖析–内核体系结构 &#169;Fanwu

Linux0.11内核剖析–内核体系结构 ©Fanwu <Linux内核完全注释>下载:http://files.cnblogs.com/files/HanBlogs/linux-kernel.pdf(进入pdf后要点击右下角保存喔^_^) 一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: 操作系统服务程序是指那些向用户所提供的服务被看作是操作系

一站式linux0.11内核head.s代码段图表详解

阅读本文章需要的基础: 计算机组成原理:针对8086,80386CPU架构的计算机硬件体系要有清楚的认知,我们都知道操作系统是用来管理硬件的,那我们就要对本版本的操作系统所依赖的硬件体系有系统的了解,有了系统的了解后才能全面的管理它,我们对8086,80386CPU架构的计算机硬件体系如果有非常深刻的认识,我们看源代码内核的时候,就可以更可能的以一种开发者的角度去思考代码的作用,先从全局的角度去思考问题,而不是采用一种众人摸象的思维从头看到末尾. 计算机编程C语言基础:linux内核基本都是用C

Linux-0.11内核内存管理get_free_page()函数分析

/* *Author : DavidLin*Date : 2014-11-11pm*Email : [email protected] or [email protected]*world : the city of SZ, in China*Ver : 000.000.001*history : editor time do 1)LinPeng 2014-11-11 created this file! 2)*/Linux-0.11内存管理模块是源代码中比较难以理解的部分,现在把笔者个人的理解

Linux-0.11内核源代码分析系列:内存管理get_free_page()函数分析

Linux-0.11内存管理模块是源码中比較难以理解的部分,如今把笔者个人的理解发表 先发Linux-0.11内核内存管理get_free_page()函数分析 有时间再写其它函数或者文件的:) /*  *Author  : DavidLin  *Date    : 2014-11-11pm  *Email   : [email protected] or [email protected]  *world   : the city of SZ, in China  *Ver     : 000

bochs+gdb联调linux-0.11内核

什么是网络营销,网络营销的方式有哪些?相信这样的问题大家谈的都比较多,每个人都能说出一二三来.然而再强的理论知识却代表不了实际情况.我们怎样去做网络营销,今天守护袁昆就以微信为切入点谈谈自己的看法. 对于网络营销的理解每个人都有偏重点,守护认为网络营销可以细分下来.网:互联网;络:交流联络;营:人员聚合;销:销售与售后.有兴趣的朋友可以去看看<什么才是真正的网络营销>.网络营销是一种通过互联网进行交流找到自己目标用户形成交易行为的过程.那么为什么有人做的好,有人却做不出效果?主要原因还是在于用