Linux Kernel 2.6.37 启动过程笔记 :set_video

http://blog.chinaunix.net/uid-1701789-id-134688.html

现在就让我们进去到现在为止我们面对的最复杂的代码set_video。望文生义,这个段代码一定做了些初始化显示设备的工作。那么让我们一起来看一下它到底是怎么干的。


set_video的代码在arch/x86/boot/Video.c里面
  1. void set_video(void)
  2. {
  3.     u16 mode = boot_params.hdr.vid_mode; //显示模式由bootloader设定
      //参见以下的1.
  1.     RESET_HEAP()

  2.   //参见以下的2.
  3.     store_mode_params();
  4.   
  5.    //参见以下3.
  6.     save_screen();
  1.    //参见以下4.
  2.     probe_cards(0);

  3.     for (;;) {
  4.         if (mode == ASK_VGA)  //如果bootloader告诉kernel具体采用什么VGA模式需要用户自己设置
  5.             mode = mode_menu();  //通知用户自己输入VGA模式
       
           //参见以下5.
  1.         if (!set_mode(mode)) //只有设置了正确的显示模式后才允许继续,否则永远循环在for里面
  2.             break;

  3.         printf("Undefined video mode number: %x\n", mode);
  4.         mode = ASK_VGA;
  5.     }
  6.     boot_params.hdr.vid_mode = mode;
      //参见6.
  1.     vesa_store_edid();
      //参见7.
  1.     store_mode_params();
      //参见8
  1.     if (do_restore)
  2.         restore_screen();
  3. }

1. 怎么又要初始化HEAP了?我们不是在main里面做过init_heap了吗?
   
我们来把RESET_HEAP从宏还原到具体代码, 这个宏在boot.h里面定义如下
  1. #define RESET_HEAP() ((void *)( HEAP = _end ))
  那么HEAP和_end又在什么地方呢?
  HEAP在main.c里面被定义为全局变量。
  1. char *HEAP = _end;
  2.   char *heap_end = _end;        /* Default end of heap = no heap */
  而_end则在我在上一篇所提到的setup.ld里。_end其实就是所有.text, .data, .bss被装载进内存之后的内存上段。
  1. . = ALIGN(16);
  2. _end = .;
所以,堆HEAP的底就是_end,这是编译器根据每次编译所需要生成的代码,变量在内存中的分布状况自动生成的。堆的顶heap_end就是init_heap里面建立的。堆的使用是从低地址向高地址分配。而栈使用是从高地址向低地址延伸。堆的底和栈的底之间的空间就是HEAP和stack可以使用的空间。
多说一句,栈的底是在header.S里面
  1. movw heap_end_ptr, %dx
  2. addw $STACK_SIZE, %dx
  3. movzwl %dx, %esp #esp现在就是栈底
栈顶就在init_heap里面
  1. asm("leal %P1(%%esp),%0"
  2.          : "=r" (stack_end) : "i" (-STACK_SIZE));
而RESET_HEAP从理论上来看是多余的,因为HEAP一早就被初始化成_end。

2. store_mode_params的定义就在Video.c里面。代码如下
  1. /*
  2.  * Store the video mode parameters for later usage by the kernel.
  3.  * This is done by asking the BIOS except for the rows/columns
  4.  * parameters in the default 80x25 mode -- these are set directly,
  5.  * because some very obscure BIOSes supply insane values.
  6.  */
  7. static void store_mode_params(void)
  8. {
  9.     u16 font_size;
  10.     int x, y;

  11.     /* For graphics mode, it is up to the mode-setting driver
  12.      (currently only video-vesa.c) to store the parameters */
  13. /*graphic_mode是一个定义在Video-mode.c里面的全局变量,我们之前知道一个定义而未被初始化的变量会放在.bss段里面,而在header.S里面,.bss段里的所有内容已经被初始化为0了,所以这里graphic_mode就应该是0, 而return其实将永远无法执行到。*/
  14.     if (graphic_mode
  15.         return;
      /*store_cursor_position实际上是调用INT 10 AH=0x03 来获取当前光标的位置并将其保存在boot_params.screen_info.orig_x和boot_params.screen_info.orig_y里面 */
  1.     store_cursor_position();
  2.    
  3.    /*store_video_mode调用的是INT 10 AH=0x0f来获取当前的显示模式通过AH来返回当前显示模式下总共有可在屏幕上显示几列字符,AL返回当前的显示模式.AL=03代表是彩色显示模式,AL=07代表黑白显示模式。BH里面返回的是当前活动的显示页面是第几个。一般的设计,显卡工作在DOS兼容模式下显示页面是有2个,一个是0,一个是1.这是由于当期,显示都是准备好一个页面然后将其激活。关于显示页面我没有找google大神和翻资料。记得大学时候老师和教材里面是这么说的,懒得查了。有人知道我错的话欢迎指出。
  4. 最终返回的AH和BH会被保存到boot_params.screen_info.orig_video_mode和boot_params.screen_info.orig_video_page中*/
  5.     store_video_mode();
    /*video_segment指向当前的显示内存,在PC中是确定的位置。具体设置的值看以下代码,它会在save_screen中被用到*/
  1.     if (boot_params.screen_info.orig_video_mode == 0x07) {
  2.         /* MDA, HGC, or VGA in monochrome mode */
  3.         video_segment = 0xb000;
  4.     } else {
  5.         /* CGA, EGA, VGA and so forth */
  6.         video_segment = 0xb800;
  7.     }

  8.     set_fs(0); //将fs段寄存器设置为0
  9.     font_size = rdfs16(0x485); /* Font size, BIOS area */ //从fs:0x485的内存处读取一个字,将其存储在font_size中。问题是fs:0x485里面是什么?
  10.     boot_params.screen_info.orig_video_points = font_size;

  11.     x = rdfs16(0x44a);//从fs:0x44a的内存处读取一个字。问题是fs:0x44a里面是什么?
  12.     y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484) 1;//从fs:0x44a的内存处读取一个字。问题是fs:0x484里面是什么?
      //至今为止force_x和force_y从来没有被引用和初始话,因为它们在bss段,现在的值就应该是0,x和y不会给改变
  1.     if (force_x)
  2.         x = force_x;
  3.     if (force_y)
  4.         y = force_y;

  5.     boot_params.screen_info.orig_video_cols = x; /*难道fs:0x44a里面就是当前显示模式下的显示列数目?*/
  6.     boot_params.screen_info.orig_video_lines = y;/*难道fs:0x484里面就是当前显示模式下的显示行数目*/
  7. }
3. 好了,显示模式已经保存在boot_params.screen_info里面了。接下来save_screen()开始了。它的工作就是保存当前的显示内存(由video_segment指出当前的显示内存的起始地址)。这段代码相对简单,就不做解释了。
  1. static void save_screen(void)
  2. {
  3.     /* Should be called after store_mode_params() */
  4.     saved.= boot_params.screen_info.orig_video_cols;
  5.     saved.= boot_params.screen_info.orig_video_lines;
  6.     saved.curx = boot_params.screen_info.orig_x;
  7.     saved.cury = boot_params.screen_info.orig_y;

  8.     if (!heap_free(saved.x*saved.y*sizeof(u16) 512))
  9.         return;        /* Not enough heap to save the screen */

  10.     saved.data = GET_HEAP(u16, saved.x*saved.y);

  11.     set_fs(video_segment);
  12.     copy_from_fs(saved.data, 0, saved.x*saved.y*sizeof(u16));
  13. }
4. 接下来就是probe_cards了
  1. /* Probe the video drivers and have them generate their mode lists. */
  2. void probe_cards(int unsafe) //当前unsafe=0
  3. {
  4.     struct card_info *card;
  5.     static u8 probed[2]; //静态未初始化变量应该分配在BSS段,并被header.S里面的bss初始化代码初始为0

  6.     if (probed[unsafe])  //这是我们第一次运行, 所以probed应该是初始值0
  7.         return;

  8.     probed[unsafe] = 1; //置位,下次将不在probe相同的Video Card


  1.     for (card = video_cards; card < video_cards_end; card++ ) {  
  2.         if (card->unsafe == unsafe) {  //这是我们要probe的卡吗?
  3.             if (card->probe)
  4.                 card->nmodes = card->probe();
  5.             else
  6.                 card->nmodes = 0;
  7.         }
  8.     }
  9. }
video_cards和video_cards_end的定义如下并在setup.ld中给出内存中的section位置,但是该值并未在以前的启动代码中被初始化,也没有看到任何对于其的引用。按照一种说法是这个值是由grub填入的,但是问题又出来了。该数组的大小是多少,怎么样分配空间的?由于下载不了grub的源码,没法继续看了。
  1. struct card_info {
  2.     const char *card_name;
  3.     int (*set_mode)(struct mode_info *mode);
  4.     int (*probe)(void);
  5.     struct mode_info *modes;
  6.     int nmodes;        /* Number of probed modes so far */
  7.     int unsafe;        /* Probing is unsafe, only do after "scan" */
  8.     u16 xmode_first;    /* Unprobed modes to try to call anyway */
  9.     u16 xmode_n;        /* Size of unprobed mode range */
  10. };

  11. #define __videocard struct card_info __attribute__((section(".videocards")))  //video_cards被关联到.videocards这个段。
  12. extern struct card_info video_cards[], video_cards_end[];
5. set_mode(mode) 其代码如下:
  1. /* Set mode (with recalc if specified) */
  2. int set_mode(u16 mode)
  3. {
  4.     int rv;
  5.     u16 real_mode;

  6.     /* Very special mode numbers... */
  7.     if (mode == VIDEO_CURRENT_MODE) //如果不想改变当前显示模式,那么什么也别干吧,没人在乎的。
  8.         return 0;    /* Nothing to do... */
  9.     else if (mode == NORMAL_VGA
  10.         mode = VIDEO_80x25; //标准VGA模式是80列25行文本显示
  11.     else if (mode == EXTENDED_VGA)
  12.         mode = VIDEO_8POINT;//扩展VGA模式是80列50行文本显示

  13.     rv = raw_set_mode(mode, &real_mode); //rv!=0则set_mode出错了。
  14.     if (rv
  15.         return rv;

  16.     if (mode & VIDEO_RECALC)
  17.         vga_recalc_vertical();//如果需要重新计算垂直方向的分辨率或字符行数。老实话,这段没看懂。有谁懂显示的吱一声。

  18.     /* Save the canonical mode number for the kernel, not
  19.      an alias, size specification or menu position */
  20. #ifndef _WAKEUP
  21.     boot_params.hdr.vid_mode = real_mode;
  22. #endif
  23.     return 0;
  24. }
raw_set_mode又涉及到video_cards和video_cards_end。看来我必须找出到底哪里初始化了video_cards和video_cards_end.
首先让我们来看一下video_cards在哪里。那么就要首先编译kernel然后使用以下命令来对于setup.elf进行检查。
  1. objdump -D setup.elf|less
  2. 我得到的结果如下:
  3. Disassembly of section .videocards:
  4. 00003a14 <video_cards>:
  5. 3a14: b1 39 mov $0x39,%cl
  6. 3a16: 00 00 add %al,(x)
  7. 3a18: a5 movsl %ds:(%esi),%es:(i)
  8. 3a19: 2c 00 sub $0x0,%al
  9. 3a1b: 00 e6 add %ah,%dh
  10. 3a1d: 2e 00 00 add %al,%cs:(x)
  11. ...
  12. 00003a30 <video_vesa>:
  13. 3a30: 08 3a or %bh,(x)
  14. 3a32: 00 00 add %al,(x)
  15. 3a34: 3e 31 00 xor x,%ds:(x)
  16. 3a37: 00 a8 2f 00 00 00 add %ch,0x2f(x)
  17. ...
  18. 3a49: 02 00 add (x),%al
  19. 3a4b: 02 0d 3a 00 00 20 add 0x2000003a,%cl
  20. 00003a4c <video_bios>:
  21. 3a4c: 0d 3a 00 00 20 or $0x2000003a,x
  22. 3a51: 34 00 xor $0x0,%al
  23. 3a53: 00 c8 add %cl,%al
  24. 3a55: 34 00 xor $0x0,%al
  25. ...
  26. 3a5f: 00 01 add %al,(x)
  27. 3a61: 00 00 add %al,(x)
  28. 3a63: 00 00 add %al,(x)
  29. 3a65: 01 .byte 0x1
  30. 3a66: 80 .byte 0x80
  31. ...
  32. Disassembly of section .data:
大家可以忽略反汇编的结果因为以上其实是数据段,反汇编的结果没有任何的意义。不过大家也能看到.videocards段里有video_cards, video_vesa和video_bios三个数据定义。对比setup.ld里面对于.videocards段的定义,我们会发现多了video_vesa和video_bios的定义而少了video_cards_end的定义。这是为什么呢?
我们再在arch/x86/boot下查找video_vesa和video_bios看它们的定义在什么地方分别找到了它们的定义在video-vesa.c和video-bios.c。里面分别有静态成员video-vesa和video-bios,而他们都引入video.h,而在video.h中明确指出了videocards段。所以他们都被放在videocards段。拿video-vesa作为例子
  1. static __videocard video_vesa =
  2. {
  3.     .card_name    = "VESA",
  4.     .probe        = vesa_probe,
  5.     .set_mode    = vesa_set_mode,
  6.     .xmode_first    = VIDEO_FIRST_VESA,
  7.     .xmode_n    = 0x200,
  8. };
你看到0x200了吗?是不是正好对应了 3a49: 02 00

^_^,找到了吗?我猜是的。而且还有一个video-vga.c的文件里面还定义了video_vga。所以我猜想videocards段里面有三个videocard结构第一个是video_vga,第二个就是video_vesa而第三个是video_bios。所以前面说的video_cards是由grub放入的应该是不对的,而且理论上grub和kernel的交流应该仅局限于hdr.

那我们再回头看一下probe_card(0),注意,每种显示方式vesa和bios的显示模式都是分配在堆里并形成数组,方法对我来说很巧妙。
  1. void probe_cards(int unsafe) //当前unsafe=0
  2. {
  3.     struct card_info *card;
  4.     static u8 probed[2]; //静态未初始化变量应该分配在BSS段,并被header.S里面的bss初始化代码初始为0

  5.     if (probed[unsafe]) //这是我们第一次运行, 所以probed应该是初始值0
  6.         return;

  7.     probed[unsafe] = 1; //置位,下次如果unsafe=0则不许要在probe了

  8.    //与其说是不同的显卡,我觉得不如说是不同的显示方式
  9.     for (card = video_cards; card < video_cards_end; card++ ) { //当前会分别probe, video_vga, video_vesa和video_bios
  10.         if (card->unsafe == unsafe) { //初始unsafe一定是0
  11.             if (card->probe)
  12.                /*probe出当前支持的显示模式。VGA会使用INT10 AH=12, 和INT10 AH=10来区分cga/ega/vga。 VESA会使用INT10 AH=4f来查询VESA的信息,因为VESA比较复杂,实际上这个软中断会把结果放在一个vesa_general_info的结构里面。 而BIOS则会直接读取内存地址的方式获得获得信息*/
  13.                 card->nmodes = card->probe(); //nmodes是当前显示方式支持的模式数目。
  14.                 /*实际probe执行的顺序是vga_probe, vesa_probe再是bios_probe.其目的是首先查询支持那些vga模式,然后查询支持那些vesa模式,然后在用BIOS查询一遍所有的模式看看有哪些是在vga, vesa之外的模式还能支持。*/
  15.             else
  16.                 card->nmodes = 0;
  17.         }
  18.     }
  19. }
好了,全局变量找到了,probe也弄清楚了,展开raw_set_mode看一下到底什么.
  1. /* Set mode (without recalc) */
  2. static int raw_set_mode(u16 mode, u16 *real_mode)
  3. {
  4.     int nmode, i;
  5.     struct card_info *card;
  6.     struct mode_info *mi;

  7.     /* Drop the recalc bit if set */
  8.     mode &= ~VIDEO_RECALC; //首先清除VIDEO_RECALC位

  9.     /* Scan for mode based on fixed ID, position, or resolution */
  10.     nmode = 0;
  11.     for (card = video_cards; card < video_cards_end; card ++) {
  12.         mi = card->modes; //对于每一种显示方式(video_cards),获取第一个支持显示模式的地址
  13.         for (= 0; i < card->nmodes; i , mi++ ) {
  14.             int visible = mi->|| mi->y;
               /*检查是否要求的mode和显示模式匹配,如果匹配就将当前的显示卡切换到要求的显示模式。但是为什么使用这样的算法进行匹配呢?由于并不了解显卡方面的知识,所以也无从得知。还好这并不妨碍我们对于总体逻辑上的判断。暂且放过吧*/
  1.             if ((mode == nmode && visible) ||
  2.              mode == mi->mode ||
  3.              mode == (mi-><< 8) mi->x) {
  4.                 *real_mode = mi->mode; //返回的实际模式的值,传入的设置模式和最终的设置模式应该是兼容的但是不完全一样,这个你可以从匹配的算法得知。
  5.                 return card->set_mode(mi);  //按照probe的注解,可以知道vga,vesa, bios的显示模式并不会重叠。所以一种模式止只可能被设置一次。
  6.             }

  7.             if (visible)
  8.                 nmode++ ;
  9.         }
  10.     }

  11.     /* Nothing found? Is it an "exceptional" (unprobed) mode? */
  12.    /*如果没有找到匹配的显示模式,那么就随便配一个吧总不至于黑屏吧*/
  13.     for (card = video_cards; card < video_cards_end; card++ ) {
  14.         if (mode >= card->xmode_first &&
  15.          mode < card->xmode_first card->xmode_n) {
  16.             struct mode_info mix;
  17.             *real_mode = mix.mode = mode;
  18.             mix.= mix.= 0;
  19.             return card->set_mode(&mix);
  20.         }
  21.     }

  22.     /* Otherwise, failure... */
  23.     return -1;
  24. }
6. 好了,我们基本能从set_mode里面回到set_video了。来看看vesa_store_edid.很显然,这就是保存显示器的edid信息到boot_params.edid_info里面,它通过INT 10 AX=4f15 BX=0x0001来实现这个读取EDID信息。其中的vgainfo结构在video_vesa.c里并定义为静态变量并且在vesa_probe里被初始化。

7. 因为已经改变过显示模式了,所以再一次运行store_mode_params保存当前的显示模式到boot_params.screen_info里

8.如果do_restore被置位,则恢复被save_screen保存的原先的video_segment内容的光标位置,为防止在切换显示模式的过程中显示内容发生变化。do_restore只在文本模式下起作用,所以vga_set_mode和bios_set_mode总会将其置为1,而vesa_set_mode的时候,如果最终切换到图形模式下则不需要置位。

好了,set_video终于结束了,接下来我们就要进入保护模式了,你准备好了吗?我们下个星期继续。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值