Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1347433
  • 博文数量: 206
  • 博客积分: 10571
  • 博客等级: 上将
  • 技术积分: 2610
  • 用 户 组: 普通用户
  • 注册时间: 2007-04-30 11:50
文章分类
文章存档

2014年(1)

2013年(4)

2012年(18)

2010年(14)

2009年(31)

2008年(3)

2007年(135)

分类: LINUX

2007-05-09 19:21:38

            uClinux移植和分析(转载)

uClinux移植和分析(1)

简介:
  前一段时间,曾先后移植了uClinux-2.0.x和uClinux-2.4.x的内核,
我的移植基本上是从零做起,linux并没有支持该目标机的代码,所以这
个移植工作基本上是新增加对一种目标机的支持。

  工作过程中,我学到了不少知识,除了操作系统,还了解了一些编
译,调试,汇编,链接的的技术,在此我会一并介绍,可能介绍比较多
的是连接器,因为这个相对和操作系统联系更加紧密一些。
  我希望能够与大家分享自己经验,同时,有错误和不当的地方欢迎
网友指出,共同进步,这是我写这些原创帖的动力。

  “编程并非零和的游戏。将己所知教给程序员同胞,他们并不会夺你
所知。能将我所知与人分享,我感到高兴,因为我身在其中、热爱编程。”
                                ——John Carmack

uClinux下用户程序的执行

  之所以从用户程序谈起,是因为我们平常接触最多的还是应用程序。
从应用程序引出到操作系统我觉得比较自然。下面就从一个简单例子介
绍一个程序如何在操作系统中运行。

  假如有个c程序:
  int main(int argc, char **argv[])
  {
    printf("hello world!\n");
    return 0;
  }

  这是一个最简单不过的程序了,一般一个C语言程序,都从main开始
执行。那么,main函数是不是与其他函数有所区别,地位有些特殊呢?
不是的。main函数和其他函数地位一样。其实,我们完全可以做到让一个
c程序从任何地方开始执行。比如linux,它就没有main函数,大家都知道,
系统执行过启动的一段汇编后,就会跳转到位于init/main.c中的
start_kernel中开始执行。

  那么为什么用户程序都要从main函数执行呢?这就是用户C库的原因。
一般用户用c语言开发时会调用一些库函数,编译成obj文件后,在链接过
程中把库函数的二进制代码链接进入程序,最后形成二进制可执行文件。
链接过程中,链接器会在用户程序前插入一些初始化的代码。uClinux下
是在crt0.s中(我移植的是uClibc库)。不管什么平台下什么形式的crt0.s,
这个文件最后几行代码中肯定有一个jmp(或者call或br等转移指令) main
(或__uClibc_main)。这就是为什么你的程序都从main开始执行。如果你把
这个跳转标号改成任意一个标号,比如foo。而你的程序里面既有main,又
有foo,则这种情况下,程序就先从foo开始执行。所以,main函数和其他
函数一样,并没有特殊地位。

  下面谈谈在uClinux中,main函数的argc,argv是参数怎样传递的。我们
以flat格式可执行文件为例。uClinux下支持一种叫flat的可执行文件格式。
这种文件格式比较简单,基本上是平铺的,所以叫flat很形象。现在好像
uClinux-2.4.x内核的版本已经能够支持elf格式的文件执行了。不过为了
举例简单,我还是用flat格式举例。这里暂不分析flat文件格式,我们把注
意力放到参数传递上。uClinux开发用户程序,首先当然是编码,然后编译,
编译生成的文件是elf格式的,所以要用工具elf2flt将elf文件转换成flat,
假设这个工作已经完成。

  我们在uclinux的shell下执行一个文件foo x y,foo是程序名,x, y是
参数。学过C语言的都知道,x,y作为参数会传递给main,其中argc=3,
argv[0]="foo", argv[1]="x", argv[2]="y"。这些参数是如何传递进来的呢。
在你执行一个程序的时候,操作系统会调用
程中把库函数的二进制代码链接进入程序,最后形成二进制可执行文件。
链接过程中,链接器会在用户程序前插入一些初始化的代码。uClinux下
是在crt0.s中(我移植的是uClibc库)。不管什么平台下什么形式的crt0.s,
这个文件最后几行代码中肯定有一个jmp(或者call或br等转移指令) main
(或__uClibc_main)。这就是为什么你的程序都从main开始执行。如果你把
这个跳转标号改成任意一个标号,比如foo。而你的程序里面既有main,又
有foo,则这种情况下,程序就先从foo开始执行。所以,main函数和其他
函数一样,并没有特殊地位。

  下面谈谈在uClinux中,main函数的argc,argv是参数怎样传递的。我们
以flat格式可执行文件为例。uClinux下支持一种叫flat的可执行文件格式。
这种文件格式比较简单,基本上是平铺的,所以叫flat很形象。现在好像
uClinux-2.4.x内核的版本已经能够支持elf格式的文件执行了。不过为了
举例简单,我还是用flat格式举例。这里暂不分析flat文件格式,我们把注
意力放到参数传递上。uClinux开发用户程序,首先当然是编码,然后编译,
编译生成的文件是elf格式的,所以要用工具elf2flt将elf文件转换成flat,
假设这个工作已经完成。

  我们在uclinux的shell下执行一个文件foo x y,foo是程序名,x, y是
参数。学过C语言的都知道,x,y作为参数会传递给main,其中argc=3,
argv[0]="foo", argv[1]="x", argv[2]="y"。这些参数是如何传递进来的呢。
在你执行一个程序的时候,操作系统会调用
do_execve(char *filename, char**argv, char**envp, struct pt_regs *regs),
这个操作会根据文件路径打开文件,装入内存,argv就是放到命令行参数,envp是
环境变量参数。

  在装入文件时,系统会根据不同的文件格式调用不同文件装入的handler,如果
是flat格式,就会调用load_flat_binary(),在fs/binfmt_flat.c中。有关参数,会
根据一路传递下来的argv,envp首先处理一遍计算出参数的个数argc,envc。然后在函
数create_flat_tables里面建立好参数表。整个函数代码如下:
static unsigned long create_flat_tables(unsigned long pp, struct linux_binprm *
bprm)
{
(1) unsigned long *argv,*envp;
(2) unsigned long * sp;
(3) char * p = (char*)pp;
(4) int argc = bprm->argc;
(5) int envc = bprm->envc;
(6) char dummy;

(7) sp = (unsigned long *) \
        ((-(unsigned long)sizeof(char *))&(unsigned long) p);

(8) sp -= envc+1;
(9) envp = sp;
(10)   sp -= argc+1;
(11)   argv = sp;

(12)   flat_stack_align(sp);
(13)   if (flat_argvp_envp_on_stack()) {
(14)     --sp; put_user((unsigned long) envp, sp);
(15)     --sp; put_user((unsigned long) argv, sp);
(16)   }

(17)   put_user(argc,--sp);
(18)   current->mm->arg_start = (unsigned long) p;
(19)   while (argc-->0) {
(20)     put_user((unsigned long) p, argv++);
(21)     do {
(22)         get_user(dummy, p); p++;
(23)     } while (dummy);
(24)   }
(25)   put_user((unsigned long) NULL, argv);
(26)   current->mm->arg_end = current->mm->env_start = (unsigned long) p;
(27)   while (envc-->0) {
(28)     put_user((unsigned long)p, envp); envp++;
(29)     do {
(30)         get_user(dummy, p); p++;
(31)     } while (dummy);
(32)   }
(33)   put_user((unsigned long) NULL, envp);
(34)   current->mm->env_end = (unsigned long) p;
(35)   return (unsigned long)sp;
}
  (1)-(6)行是变量声明。其中argc和envc分别记录前面已经计算出来的参数个数和
环境变量参数个数。p=pp是参数和环境变量数组的指针,sp是你要执行程序的用户区
堆栈,就是foo程序执行时,用户空间堆栈的起始地址。(8)-(11)是一个堆栈调整。首
先sp移动envc+1个单位,这envc+1个用来存放一共envc个envp[0]->envc[envp-1]元素
地址的,多余一个放0,表示envp数组结束。然后sp在移动argc+1各单位,留出argc+1
单位空间,这argc+1个单位是用来存放argc个argv[0]->argv[argc-1]元素地址的,多
余一个也放0,表示argv数组结束。经过堆栈调整,argv和envp各自指向自己在堆栈中
的位置。如果开始堆栈初值记为init_sp,则现在envp=init_sp-(envc+1),
argv=envp-(argc+1)。

  (12)无关紧要,略去不提。(13)-(17)又是一次堆栈调整。(14)是sp再移动1个单
位,然后将envp放入这个地址(此时envp=init_sp-(envc+1)),然后(15)又将sp移动一个
单位,将argv写入. (17)是移动堆栈后将argc也写入里面.

  (18)-(35)行是将argv[0]->argv[argc-1](在p所指向地方)依次写入argv所指堆栈
区域中.然后再将envp[0]->edummy, p); p++;
(31)     } while (dummy);
(32)   }
(33)   put_user((unsigned long) NULL, envp);
(34)   current->mm->env_end = (unsigned long) p;
(35)   return (unsigned long)sp;
}
  (1)-(6)行是变量声明。其中argc和envc分别记录前面已经计算出来的参数个数和
环境变量参数个数。p=pp是参数和环境变量数组的指针,sp是你要执行程序的用户区
堆栈,就是foo程序执行时,用户空间堆栈的起始地址。(8)-(11)是一个堆栈调整。首
先sp移动envc+1个单位,这envc+1个用来存放一共envc个envp[0]->envc[envp-1]元素
地址的,多余一个放0,表示envp数组结束。然后sp在移动argc+1各单位,留出argc+1
单位空间,这argc+1个单位是用来存放argc个argv[0]->argv[argc-1]元素地址的,多
余一个也放0,表示argv数组结束。经过堆栈调整,argv和envp各自指向自己在堆栈中
的位置。如果开始堆栈初值记为init_sp,则现在envp=init_sp-(envc+1),
argv=envp-(argc+1)。

  (12)无关紧要,略去不提。(13)-(17)又是一次堆栈调整。(14)是sp再移动1个单
位,然后将envp放入这个地址(此时envp=init_sp-(envc+1)),然后(15)又将sp移动一个
单位,将argv写入. (17)是移动堆栈后将argc也写入里面.

  (18)-(35)行是将argv[0]->argv[argc-1](在p所指向地方)依次写入argv所指堆栈
区域中.然后再将envp[0]->envp[envc-1](也是由p所指)写入envp所指的堆栈区域中.
在写入同时,还要设置进程控制块相应的数据结构,如arg_start,env_start,env_end等.

  下面举例和画图来说明过程.比如执行foo x y,此时argc=3,argv[0]="foo",
argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假设用户堆栈起始
空间堆栈地址是sp=0x1f0000,pp=0x1c0000.则处理过后在foo执行前,他的用户空
间堆栈frame如下:


      --------------------------------
0x1f0000   |       0000           |
      --------------------------------
0x1efffc   | envp[0] = 0x1c0008       | ---->指向"path=/bin"
      --------------------------------
0x1efff8   |       0000           |
      --------------------------------
0x1efff4   | argv[2] = 0x1c0006       | ----->指向"y"
      --------------------------------
0x1efff0   | argv[1] = 0x1c0004       | ----->指向"x"
      --------------------------------
0x1effec   | argv[0] = 0x1c0000       | ----->指向"foo"
      --------------------------------
0x1effe8   | start addr of envp = 0x1efffc|
在写入同时,还要设置进程控制块相应的数据结构,如arg_start,env_start,env_end等.

  下面举例和画图来说明过程.比如执行foo x y,此时argc=3,argv[0]="foo",
argv[1]="x", argv[2]="y", envc=1, envp[0]="path=/bin". 假设用户堆栈起始
空间堆栈地址是sp=0x1f0000,pp=0x1c0000.则处理过后在foo执行前,他的用户空
间堆栈frame如下:


      --------------------------------
0x1f0000   |       0000           |
      --------------------------------
0x1efffc   | envp[0] = 0x1c0008       | ---->指向"path=/bin"
      --------------------------------
0x1efff8   |       0000           |
      --------------------------------
0x1efff4   | argv[2] = 0x1c0006       | ----->指向"y"
      --------------------------------
0x1efff0   | argv[1] = 0x1c0004       | ----->指向"x"
      --------------------------------
0x1effec   | argv[0] = 0x1c0000       | ----->指向"foo"
      --------------------------------
0x1effe8   | start addr of envp = 0x1efffc|
到r2-r6里来传递。当然,如果超过5个,就要借助堆栈了。

  既然main带了参数,那么在调用main之前,要把argc放到r2里面,argv放到r3里
面,envp放到r4里面。刚才说了,sp是用户空间堆栈起始地址。所以在开始执行foo
代码时候,r0=sp,在上文例子里r0等于0x1effe0.则如下伪汇编代码可以让参数装入
正确寄存器。

  load   r2, (r0)     /* r2 = argc */
  load   r3, (r0, 4)   /* r3 = argv */
  load   r4, (r0, 8)   /* r4 = envp */
  call   main       /* 跳转到main函数 */

  call   _exit

  以上代码就是最简单的进入main函数前的预处理。当然,不同系统不同格式文件处
理方式是不同的,刚才的一些例子是我碰到的一些情景和解决方案。

  我这个程序例子还没有完全讲完,比如后面printf怎么处理等,不过手都酸了,就
先讲到main函数的参数传递吧。刚学c语言那阵觉得main挺神秘,做过系统就知道,其实
main跟别的函数没有任何区别:)

  写了半天,头晕脑胀,肯定还有一些错误或者不够优化的地方,欢迎拍砖。没讲清楚

uClinux移植和分析(2)

printf和标准输出

  上次写到main函数的参数传递.现在继续往下进行.最近忙实验室的事情,看了一周
的文章,也没啥进展,周末写点技术贴,放松一下:-)

  进入main函数后,就要调用printf("Hello World!\n");了.顺便将C语言参数传递提
一下.字符串"Hello World!\n"编译器是当作字符串常量来处理的,虽然printf是在main
内部调用,但"Hello World!\n"可不是放在main的栈中,字符串常量至少是放到.data段的
,准确说是放在只读数据段.rodata,这个我在工作站上验证了一把.假如编辑的文件名是
hello.c,首先编译生成elf格式二进制文件gcc hello.c -o hello然后用命令
objdump -s hello -s参数会将所有段信息dump出来.你会看到"Hello World!\n"位于
.rodata段.

  printf()是个标准C库函数.虽然功能简单,但实现起来却不容易.这是个和平台相关的
函数.在pc上,printf输出是输出到终端屏幕,在嵌入式设备上,一般printf()是输出到串口.
同是调用printf(),最终输出的设备却不同,从直觉的肯定是感觉printf()底层和平台是相
关的.那么printf()是怎样实现的呢?

  可以看一下C库程序的代码,这里以uClibc为例.
  int printf(const char * __restrict format, ...)
  {
  va_list arg;
  int rv;

  va_start(arg, format);
  rv = vfprintf(stdout, format, arg);
  va_end(arg);

  return rv;
  }
  printf支持字符串格式化输出,具体参数处理这里不提.可以看到printf()调用了
vfprintf(),vfprintf()第一个参数是stdout是标准输出设备.标准输出设备是个结构体,
最重要的成员就是他的描述符,其值为1.

  跟进vfprintf()函数看,里面是复杂的参数处理,因为printf()的参数形式很灵活,
所以在vfprintf()里面要对传进来的参数进行解析处理,形成最终的输出格式.有兴趣的
可以看一下,借助这个可以让你在一个没有操作系统的平台上实现你自己的printf()
函数.这样你在裸机上调程序时输出调试信息就更方便些(实际上uClinux的printk就是
这么干的).

  vfprintf()在参数处理之后,就是输出了,输出调用的是putc(),进入putc()然后
再跟进几层函数,发现调用了linux系统调用write()。呵呵,是的,输出就是借助操作
系统代码完成的。在write之前所有的代码都是C库的代码,可以说是和平台无关的。
而涉及到具体输出,就要调用操作系统提供给你的接口。系统调用的原理就是通过一定
手段(一般是trap陷阱)进入操作系统的内核空间,调用操作系统代码来完成某些任务。

  Linux系统调用针对不同平台有不同的实现方式。这个以后再讲。调用write()后,
进入内核空间,首先来到的就是sys_write(),这个函数代码位于fs/read_write.c中。
一进入sys_write(),就要根据传进来的fd描述符找到相应的file结构。对于标准输出,
fd=1,每个进程的进程控制块都有一个打开文件的数组。file结构就是根据fd在这个
数组中查找到相应的结构。找到结构后,就会调用file->write()来向外输出。具体输出
到哪里,就要看file结构对应的设备驱动是什么。一般嵌入式系统可以从串口将信息输
出,那么file->write()最底层就是调用的串口驱动的类似transmit_char的函数。

  有关linux的设备驱动有很多书介绍,整个驱动的结构很复杂,我这里也没必要提了.
至于终端设备怎样挂在驱动队列里面,怎么根据标准输出的描述符找到相应的驱动结构
有兴趣的莊printf()函数看,里面是复杂的参数处理,因为printf()的参数形式很灵活,
所以在vfprintf()里面要对传进来的参数进行解析处理,形成最终的输出格式.有兴趣的
可以看一下,借助这个可以让你在一个没有操作系统的平台上实现你自己的printf()
函数.这样你在裸机上调程序时输出调试信息就更方便些(实际上uClinux的printk就是
这么干的).

  vfprintf()在参数处理之后,就是输出了,输出调用的是putc(),进入putc()然后
再跟进几层函数,发现调用了linux系统调用write()。呵呵,是的,输出就是借助操作
系统代码完成的。在write之前所有的代码都是C库的代码,可以说是和平台无关的。
而涉及到具体输出,就要调用操作系统提供给你的接口。系统调用的原理就是通过一定
手段(一般是trap陷阱)进入操作系统的内核空间,调用操作系统代码来完成某些任务。

  Linux系统调用针对不同平台有不同的实现方式。这个以后再讲。调用write()后,
进入内核空间,首先来到的就是sys_write(),这个函数代码位于fs/read_write.c中。
一进入sys_write(),就要根据传进来的fd描述符找到相应的file结构。对于标准输出,
fd=1,每个进程的进程控制块都有一个打开文件的数组。file结构就是根据fd在这个
数组中查找到相应的结构。找到结构后,就会调用file->write()来向外输出。具体输出
到哪里,就要看file结构对应的设备驱动是什么。一般嵌入式系统可以从串口将信息输
出,那么file->write()最底层就是调用的串口驱动的类似transmit_char的函数。

  有关linux的设备驱动有很多书介绍,整个驱动的结构很复杂,我这里也没必要提了.
至于终端设备怎样挂在驱动队列里面,怎么根据标准输出的描述符找到相应的驱动结构
有兴趣的请查阅相关资料.
--


手段(一般是trap陷阱)进入操作系统的内核空间,调用操作系统代码来完成某些任务。

  Linux系统调用针对不同平台有不同的实现方式。这个以后再讲。调用write()后,
进入内核空间,首先来到的就是sys_write(),这个函数代码位于fs/read_write.c中。
一进入sys_write(),就要根据传进来的fd描述符找到相应的file结构。对于标准输出,
fd=1,每个进程的进程控制块都有一个打开文件的数组。file结构就是根据fd在这个
数组中查找到相应的结构。找到结构后,就会调用file->write()来向外输出。具体输出
到哪里,就要看file结构对应的设备驱动是什么。一般嵌入式系统可以从串口将信息输
出,那么file->write()最底层就是调用的串口驱动的类似transmit_char的函数。

  有关linux的设备驱动有很多书介绍,整个驱动的结构很复杂,我这里也没必要提了.
至于终端设备怎样挂在驱动队列里面,怎么根据标准输出的描述符找到相应的驱动结构
有兴趣的请查阅相关资料.
--


uClinux移植与分析(3)

进程切换部分代码实现

  移植linux,修改的主要就是和平台相关的那部分代码.linux里面和平台相关的代码
包括很多方面,比如boot过程,系统调用,中断处理,设备驱动,还有部分信号(软中断)处理
等,进程切换也有很小一部分平台相关代码.相对其它部分,我觉得这部分平台相关代码还
是相对简单的.

  schedule()是uClinux中实现进程调度的函数.通过一定算法,进行调度.假设有2各进
程a,b,a运行时,调用了schedule(),那么os就要从进程就绪队列中挑选一个合适的进程,
如果没有合适进程,则后面继续运行a,假设找到了合适进程b,则就要从当前进程a切换到b.
这个切换过程是在switch_to()中进行的.

  switch_to()出现在schedule()函数里面。调用形式switch_to(prev, next, last);
prev,next都是进程控制块task_struct的指针.prev指向当前运行的进程,next指向要切
换的进程.

  讲一下我移植的代码.由于代码是汇编程序,首先介绍一下cpu结构。我用的cpu采用
16位指令,32位的地址和数据。有16个通用寄存器,记作r0-r15。r0作为堆栈指针寄存器
sp,r1用途不固定,r2-r6作为参数传递寄存器,函数调用如果有不超过5个的参数,则参
数从左至右依次放在r2-r6中。同时,r2还作为函数返回值寄存器,函数的返回值都放在
r2里面。r7-r14是局部变量寄存器。r15是函数返回地址寄存器,也叫link register,
存放的是function call地返回地址。

  #define switch_to(prev,next,last) {           \
(1) register void *_prev __asm__ ("r2") = (prev);   \
(2) register void *_next __asm__ ("r3") = (next);   \
(3) register void *_last;                   \
(4)       __asm__ __volatile__(             \
(5)       "jbsr " SYMBOL_NAME_STR(resume) "\n\t" \
(6)       "mfcr %0, ss4"                 \
(7)         : "=r" (_last)               \
(8)         : "r" (_prev),               \
(9)           "r" (_next)                 \
(10)         : "r2", "r2", "r3");           \
(11) (last) = _last;                       \
  }
换的进程.

  讲一下我移植的代码.由于代码是汇编程序,首先介绍一下cpu结构。我用的cpu采用
16位指令,32位的地址和数据。有16个通用寄存器,记作r0-r15。r0作为堆栈指针寄存器
sp,r1用途不固定,r2-r6作为参数传递寄存器,函数调用如果有不超过5个的参数,则参
数从左至右依次放在r2-r6中。同时,r2还作为函数返回值寄存器,函数的返回值都放在
r2里面。r7-r14是局部变量寄存器。r15是函数返回地址寄存器,也叫link register,
存放的是function call地返回地址。

  #define switch_to(prev,next,last) {           \
(1) register void *_prev __asm__ ("r2") = (prev);   \
(2) register void *_next __asm__ ("r3") = (next);   \
(3) register void *_last;                   \
(4)       __asm__ __volatile__(             \
(5)       "jbsr " SYMBOL_NAME_STR(resume) "\n\t" \
(6)       "mfcr %0, ss4"                 \
(7)         : "=r" (_last)               \
(8)         : "r" (_prev),               \
(9)           "r" (_next)                 \
(10)         : "r2", "r2", "r3");           \
(11) (last) = _last;                       \
  }

  switch_to()所做的工作其实相当于为调用resume做一些准备。(1)-(2)的意思是将变
量_prev,_next分别放在寄存器r2,r3里面,他们的值分别等于prev和next,就是两个
task_struct的指针。这么做是为调用resume准备好参数。第三行是声明一个寄存器临时变
量_last。

  第(5)行是调用resume函数实现进程切换。jbsr是一条跳转指令,字面意思是跳入到子
程序(jump to subroutine),这条指令做的工作是将现将当前pc+2保存到r15中(因为是16
位指令,所以+2),相当于保存函数的返回值,然后再将pc设置成汇编指令参数中给出的
地址(就是跳转,这里就是resume的地址)。

  第(6)行是将控制寄存器ss4内容放到_last对应的寄存器中。这一行指令有一些
trick,先讲指令所做的操作,再讲为什么这样做。mfcr是从控制寄存器移动到通用寄
存器的指令。cpu除了有16个通用寄存器,还有16各控制寄存器。所有涉及控制寄存器
的操作都要在cpu的超级用户模式下进行。cpu模式切换通过设置第0号控制寄存器来完
成。16个控制寄存器分别为cr0-cr15,其中cr0也叫psr是程序状态寄存器。cr6-cr10
也叫ss0-ss4是用于保存状态的寄存器。第(6)代码就是将ss4内容放入到变量_last
所对应的寄存器中。

  (7)-(10)行的意义请参考AT&T汇编。

  (11)行是一个赋值,last=_last。

  switch_to()所做的工作其实相当于为调用resume做一些准备。(1)-(2)的意思是将变
量_prev,_next分别放在寄存器r2,r3里面,他们的值分别等于prev和next,就是两个
task_struct的指针。这么做是为调用resume准备好参数。第三行是声明一个寄存器临时变
量_last。

  第(5)行是调用resume函数实现进程切换。jbsr是一条跳转指令,字面意思是跳入到子
程序(jump to subroutine),这条指令做的工作是将现将当前pc+2保存到r15中(因为是16
位指令,所以+2),相当于保存函数的返回值,然后再将pc设置成汇编指令参数中给出的
地址(就是跳转,这里就是resume的地址)。

  第(6)行是将控制寄存器ss4内容放到_last对应的寄存器中。这一行指令有一些
trick,先讲指令所做的操作,再讲为什么这样做。mfcr是从控制寄存器移动到通用寄
存器的指令。cpu除了有16个通用寄存器,还有16各控制寄存器。所有涉及控制寄存器
的操作都要在cpu的超级用户模式下进行。cpu模式切换通过设置第0号控制寄存器来完
成。16个控制寄存器分别为cr0-cr15,其中cr0也叫psr是程序状态寄存器。cr6-cr10
也叫ss0-ss4是用于保存状态的寄存器。第(6)代码就是将ss4内容放入到变量_last
所对应的寄存器中。

  (7)-(10)行的意义请参考AT&T汇编。

  (11)行是一个赋值,last=_last。

  其实,上面并不是一个非常优化的做法。完全可以省掉_last变量,不过当初我做时
,看到m68k版本用了_last变量,而又不很清楚他的作用,为防止出错,照办了过来。其
实经过后面分析,可知这个变量其实是冗余的。

  那么,为什么要有(6)和(11)行的代码呢?回头可以看一下schedule()的代码,在
switch_to()调用过后,schedule()中调用了schedule_tail(prev)函数。显然prev作为
参数,应该放到r2里面,所以就有了switch_to()代码的第(11)行。那么为什么prev是来
自ss4呢?

  在调用resume之前,prev存放在r2中。r2中的内容属于进程的上下文,在做进程切
换时,要存放在栈中。同时切换到另一个进程时,还要将另一个进程的上下文装入到寄
存器中。在装入新进程时,r2的值就会被冲掉。举个例子,比如你通过fork系统调用创
建了一个新进程。我们知道,fork地返回值如果是0就表示子进程,大于0就是父进程。
对于子进程,这个栈里r2就是0(前面说过,r2用作放函数返回值),如果此时schedule
选了一个经fork后的子进程开始执行,则切换到该子进程后,其r2显然为0,当然就不
是prev了。所以,我的实现是在进程切换时,将r2值存放在ss4中,切换完毕后,再进
行区别对待。如果是两个已经运行过的进程切换,则返回就返回到原来switch_to的地
方。如果是新的fork出来的进程,则第一次调用,在resume返回时,返回的是
ret_from_fork,这是另外处理的。

  (11)行是一个赋值,last=_last。

  其实,上面并不是一个非常优化的做法。完全可以省掉_last变量,不过当初我做时
,看到m68k版本用了_last变量,而又不很清楚他的作用,为防止出错,照办了过来。其
实经过后面分析,可知这个变量其实是冗余的。

  那么,为什么要有(6)和(11)行的代码呢?回头可以看一下schedule()的代码,在
switch_to()调用过后,schedule()中调用了schedule_tail(prev)函数。显然prev作为
参数,应该放到r2里面,所以就有了switch_to()代码的第(11)行。那么为什么prev是来
自ss4呢?

  在调用resume之前,prev存放在r2中。r2中的内容属于进程的上下文,在做进程切
换时,要存放在栈中。同时切换到另一个进程时,还要将另一个进程的上下文装入到寄
存器中。在装入新进程时,r2的值就会被冲掉。举个例子,比如你通过fork系统调用创
建了一个新进程。我们知道,fork地返回值如果是0就表示子进程,大于0就是父进程。
对于子进程,这个栈里r2就是0(前面说过,r2用作放函数返回值),如果此时schedule
选了一个经fork后的子进程开始执行,则切换到该子进程后,其r2显然为0,当然就不
是prev了。所以,我的实现是在进程切换时,将r2值存放在ss4中,切换完毕后,再进
行区别对待。如果是两个已经运行过的进程切换,则返回就返回到原来switch_to的地
方。如果是新的fork出来的进程,则第一次调用,在resume返回时,返回的是
ret_from_fork,这是另外处理的。

  上面说了这么多,可能读者还是糊里糊涂的,我也觉得自己没说清楚,所以这里的
这点实现有那么一点点trick,需要对cpu的ABI和linux的内核代码非常熟悉才行。
    (11)ldw   r7, (r0)         /* restore r7 */
    (12)ldw   r8, (r0, 4)       /* restore r8 */
    (13)addi   r0, 8
    (14)SAVE_SWITCH_STACK
    (15)lrw   r8, TASK_THREAD   /* the position of thread in task_struct */
    (16)addu   r8, r2
    (17)mfcr   r6, ss1           /* Get current usp */
    (18)stw   r6, (r8, THREAD_USP) /* Save usp in task struct */
    (19)stw   r0, (r8, THREAD_KSP) /* Save ksp in task struct */

    (20)lrw   r8, TASK_THREAD
    (21)lrw   r7, SYMBOL_NAME(_current_task)
    (22)stw   r3, (r7)         /* Set new task */
    (23)addu   r8, r3           /* Pointer to thread in task_struct */

    /* Set up next process to run */
    (24)ldw   r0, (r8, THREAD_KSP) /* Set next ksp */
    (25)ldw   r6, (r8, THREAD_USP) /* Set next usp */
    (26)mtcr   r6, ss1
    (27)ldw   r7, (r8, THREAD_SR)   /* Set next PSR */
    (28)mtcr   r7, psr
    (29)RESTORE_SWITCH_STACK
              ----------------
              |   r11     |
              ----------------
              |   r10     |
              ----------------
              |   r9     |
              ----------------
              |   r8     |
              ----------------
              |   r7     |
              ----------------
              |   r6     |
              ----------------
              |   r5     |
              ----------------
              |   r4     |
              ----------------
              |   r3     |
              ----------------
              |   r2     |
              ----------------
        0x1effC4 |   r1     |
              ----------------
0x1f0000和0x1effc4分别是执行过(14)前后r0的值。这是个contex save的操作。

注:lrw是立即数装入操作,addu是无符号加法,mfcr和mtcr是控制寄存器移动
  操作,bclri是位清除操作,ldw是load word操作,addi是立即数加法操作。

  (15)-(19)是做的栈指针保存操作。将当前进程用户栈和内核栈保存到进程
控制块相应的数据结构中。linux下除了内核线程(只有内核栈)每个进程都有
2个栈,一个在用户空间一个在内核空间。如果是内核线程,则不用关心它的用
户堆栈,反正不会用到,是什么值都可以。如果用户进程,则在用户进程执行
系统调用或者在用户进程执行时发生中断时,都需要从用户空间进入内核空间,
在进入时,原先的用户空间栈指针就会暂时存放到ss1中。所以(17)-(18)两行
就是从ss1中取出用户空间栈指针,存入task_struct中。(15)-(19)的操作可
以总结为:
  prev->thread.usp = ss1 保存用户栈指针
  prev->thread.ksp = r0   保存内核栈指针

  那么有人可能会问,ss1能够保证就是正确的当前用户栈指针么?当然可以。
因为内核线程没有用户栈,所以这个值是什么无所谓。而对于用户进程,进入
resume的唯一入口就是schedule,而这又都是操作系统内核代码。用户进程进
入内核手段就有系统调用和中断,而在系统调用和中断处理一进来就保存了用
户堆栈到ss1,所以在运行时,只要在内核里用的都是内核栈,用户栈指针不会
变。

  (20)-(23)执行的操作相当于_current_task = next。不再详细解释。

  (24)-(28)执行的是装入新进程上下文的准备工作,也就是准备装入next了。
(24)-(25)是装入next进程的内核和用户栈。因为进程的上下文都保存在该进程
的内核栈里面,所以第一步就是装入该进程的栈指针。(27)-(28)是装入next进
程在切换前的状态信息。(26)就是更新ss1,现在要装入新进程了,当然就要设置
新的用户栈。

  (29)是装入next进程的上下文。next进程在栈里有一个和上图一样的上下文,
现在就要装入。

  (30)是函数调用返回。如果这个进程是刚fork出来的子进程,则上下文里面
r15=ref_from_fork(可以参看copy_thread函数),否则就是返回到switch_to里
面第(6)句位置。

  上面就是进程切换的部分。这部分是和平台相关的。以上是我实现的代码,
感觉效率并不是非常高,但功能是正确的。可能有些地方我没有讲得很清楚,有
什么问题欢迎提出。
--

新的用户栈。

  (29)是装入next进程的上下文。next进程在栈里有一个和上图一样的上下文,
现在就要装入。

  (30)是函数调用返回。如果这个进程是刚fork出来的子进程,则上下文里面
r15=ref_from_fork(可以参看copy_thread函数),否则就是返回到switch_to里
面第(6)句位置。

  上面就是进程切换的部分。这部分是和平台相关的。以上是我实现的代码,
感觉效率并不是非常高,但功能是正确的。可能有些地方我没有讲得很清楚,有
什么问题欢迎提出。
--


Spec->RTL->Verification->Synthesis->Backend Veri->Place & Route->Layout
->Tapeout->Foundry->Test->Software->Debug->Sell->Use->Break->Repair or
Throw away

※ 内容修改:·swankong 於 Dec 4 22:13:18 修改本文内容·[FROM: swankong]
※ 来源:·飘渺水云间 freecity.cn·[FROM: swankong]

阅读(833) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~