接下来main将执行进入保护模式前最重要的一个步骤,设置视频模式,调用set_video()函数,来自linux/arch/x86/boot/video.c:
315void set_video(void) 316{ 317 u16 mode = boot_params.hdr.vid_mode; 318 319 RESET_HEAP(); 320 321 store_mode_params(); 322 save_screen(); 323 probe_cards(0); 324 325 for (;;) { 326 if (mode == ASK_VGA) 327 mode = mode_menu(); 328 329 if (!set_mode(mode)) 330 break; 331 332 printf("Undefined video mode number: %x/n", mode); 333 mode = ASK_VGA; 334 } 335 boot_params.hdr.vid_mode = mode; 336 vesa_store_edid(); 337 store_mode_params(); 338 339 if (do_restore) 340 restore_screen(); 341} |
317行,根据传过来的hdr参数得到视频模式,存储到内部变量mode中。我们看到header.S中的vid_mode值是SVGA_MODE。这时候要从新设置一下堆的位置,把它设定到_end处。
为什么要调整一下堆的位置,我也搞不清楚,也许是跟视频配置有着千丝万缕的关系吧,有兴趣的同志可以去研究一下。随后,调用store_mode_params()来设置boot_params的screen_info字段:
53/* 54 * Store the video mode parameters for later usage by the kernel. 55 * This is done by asking the BIOS except for the rows/columns 56 * parameters in the default 80x25 mode -- these are set directly, 57 * because some very obscure BIOSes supply insane values. 58 */ 59static void store_mode_params(void) 60{ 61 u16 font_size; 62 int x, y; 63 64 /* For graphics mode, it is up to the mode-setting driver 65 (currently only video-vesa.c) to store the parameters */ 66 if (graphic_mode) 67 return; 68 69 store_cursor_position(); 70 store_video_mode(); 71 72 if (boot_params.screen_info.orig_video_mode == 0x07) { 73 /* MDA, HGC, or VGA in monochrome mode */ 74 video_segment = 0xb000; 75 } else { 76 /* CGA, EGA, VGA and so forth */ 77 video_segment = 0xb800; 78 } 79 80 set_fs(0); 81 font_size = rdfs16(0x485); /* Font size, BIOS area */ 82 boot_params.screen_info.orig_video_points = font_size; 83 84 x = rdfs16(0x44a); 85 y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1; 86 87 if (force_x) 88 x = force_x; 89 if (force_y) 90 y = force_y; 91 92 boot_params.screen_info.orig_video_cols = x; 93 boot_params.screen_info.orig_video_lines = y; 94} |
store_mode_params()函数主要是利用BIOS显示服务程序对视频显示进行设置。有关BIOS显示服务程序请参考有关博文“BIOS系统服务 —— 显示服务”
首先,我们看到69行store_cursor_position()函数:
20static void store_cursor_position(void) 21{ 22 struct biosregs ireg, oreg; 23 24 initregs(&ireg); 25 ireg.ah = 0x03; 26 intcall(0x10, &ireg, &oreg); 27 28 boot_params.screen_info.orig_x = oreg.dl; 29 boot_params.screen_info.orig_y = oreg.dh; 30 31 if (oreg.ch & 0x20) 32 boot_params.screen_info.flags |= VIDEO_FLAGS_NOCURSOR; 33 34 if ((oreg.ch & 0x1f) > (oreg.cl & 0x1f)) 35 boot_params.screen_info.flags |= VIDEO_FLAGS_NOCURSOR; 36} |
第25行,AH为03表示在文本坐标下,读取光标各种信息功能。出口参数oreg为:CH=光标的起始行;CL=光标的终止行;DH=行(Y坐标);DL=列(X坐标)。那么将DL和DH的值分别存储到boot_params参数的screen_info.orig_x字段和screen_info.orig_y中,表示当前光标的位置,并根据结果修改screen_info的flags。
回到store_mode_params()函数中,接下来执行store_video_mode()函数
38static void store_video_mode(void) 39{ 40 struct biosregs ireg, oreg; 41 42 /* N.B.: the saving of the video page here is a bit silly, 43 since we pretty much assume page 0 everywhere. */ 44 initregs(&ireg); 45 ireg.ah = 0x0f; 46 intcall(0x10, &ireg, &oreg); 47 48 /* Not all BIOSes are clean with respect to the top bit */ 49 boot_params.screen_info.orig_video_mode = oreg.al & 0x7f; 50 boot_params.screen_info.orig_video_page = oreg.bh; 51} |
第45行,AH为0f表示读取显示器模式功能,出口参数oreg中,ah为屏幕字符的列数;al为显示模式;bh=页码。随后将这些信息分别存储到screen_info的orig_video_mode和orig_video_page中,表示当前的显示属性。
回到store_mode_params()函数中,第72到78行,没有问题,根据刚刚得到的显示模式,设置全局变量video_segment的值。同样,这个全局变量跟_end一样,是编译的时候生成的。第80、81行,从BIOS中获取字体大小,保存到内部变量font_size中。注意,为什么说是从BIOS中,因为调用rdfs16内联函数:
static inline u16 rdfs16(addr_t addr)
{
u16 v;
asm volatile("movw %%fs:%1,%0" : "=r" (v) : "m" (*(u16 *)addr));
return v;
}
其参数addr 的值为0x485,对照内核映像解压缩后的内存布局,可以看到这个地址是在BIOS的区域内。82行的代码就保留这个值。
84到93行,仍然是通过直接读BIOS的方式得到显示属性的行数和列数,并填充screen_info的orig_video_cols和orig_video_lines字段。注意这里有个编译选项,如果在编译的时候设定了force_x或force_y,就把显示属性的行数和列数强制设为这个值。
store_mode_params()函数结束了,基本的显示参数都存储到boot_params的screen_info字段中了,回到set_video函数中,第322行,save_screen()函数,用于将当前屏幕的内容存储到指定的内存空间中:
230/* Save screen content to the heap */ 231static struct saved_screen { 232 int x, y; 233 int curx, cury; 234 u16 *data; 235} saved; 236 237static void save_screen(void) 238{ 239 /* Should be called after store_mode_params() */ 240 saved.x = boot_params.screen_info.orig_video_cols; 241 saved.y = boot_params.screen_info.orig_video_lines; 242 saved.curx = boot_params.screen_info.orig_x; 243 saved.cury = boot_params.screen_info.orig_y; 244 245 if (!heap_free(saved.x*saved.y*sizeof(u16)+512)) 246 return; /* Not enough heap to save the screen */ 247 248 saved.data = GET_HEAP(u16, saved.x*saved.y); 249 250 set_fs(video_segment); 251 copy_from_fs(saved.data, 0, saved.x*saved.y*sizeof(u16)); 252} |
一路走来的朋友们对这种函数应该熟悉了吧,240行道248行设置全局变量saved。有个地方不看不懂,就是GET_HEAP:
#define RESET_HEAP() ((void *)( HEAP = _end ))
static inline char *__get_heap(size_t s, size_t a, size_t n) { char *tmp;
HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1)); tmp = HEAP; HEAP += s*n; return tmp; } #define GET_HEAP(type, n) / ((type *)__get_heap(sizeof(type),__alignof__(type),(n))) |
像这么复杂的代码在Linux中比比皆是。虽然我们看不懂,但是可以猜。不过就像当年我们学英语做阅读题一样,必须有根据地猜,不能瞎猜。首先,还记得先前set_video的319行有个RESET_HEAP,就是把HEAP全局变量设置成了_end。_end这个东西我们熟悉,是vmlinuz实模式部分的结束位置,也是堆栈段的开始位置。调用GET_HEAP宏之前,HEAP指向的_end。而调用GET_HEAP(u16, saved.x*saved.y),对应__get_heap返回后,HEAP大约就指向了_end后再经过2*saved.x*saved.y*2字节偏移的位置(我们假设sizeof(u16)为2)。
因此,我猜测,屏幕假设有saved.x*saved.y个像素,那么每个像素需要2*2即4个字节的内存来存储,248行使用GET_HEAP宏就是分配这么个堆来存储这些东西,并且saved得data字段指向这个堆的首地址。顺便提一下,学过操作系统的都知道,堆跟栈是差不多的概念,唯一不同的是栈是向上增长的,堆跟栈正好相反,向下增长。所以这个堆用在这里,给这个多分配了这么多空间,就不会与其他内存中的内容发生冲突。
save_screen()函数的最后一行,调用一个copy_from_fs函数将当前屏幕的每个像素的内容拷贝到这个堆中。注意,video_segment是grub传递过来的参数,记录着内存中显示器设备的内容。
回到set_video中,接下来315行调用probe_cards(0),用来扫描显卡,来自
linux/arch/x86/boot/video-mode.c:
/* Probe the video drivers and have them generate their mode lists. */ 33void probe_cards(int unsafe) 34{ 35 struct card_info *card; 36 static u8 probed[2]; 37 38 if (probed[unsafe]) 39 return; 40 41 probed[unsafe] = 1; 42 43 for (card = video_cards; card < video_cards_end; card++) { 44 if (card->unsafe == unsafe) { 45 if (card->probe) 46 card->nmodes = card->probe(); 47 else 48 card->nmodes = 0; 49 } 50 } 51} |
传递给probe_cards的参数为0,所以unsafe=0。随后扫描整个显卡列表。video_cards和video_cards_end都是grub传递过来的显卡列表。
回到set_video中,325行进入一个循环。根据bootloader传进来的hdr的vid_mode,如果mode为ASK_VGA,就进行一些交互式的工作。grub传进来的vid_mode不是ASK_VGA,所以我们不去分析mode_menu()函数了,感兴趣的,或者想了解BIOS编程的可以去分析一下它,这个函数是交互式BIOS程序的编程典范。
直接跳出循环,来到336行。vesa_store_edid()函数,是对EDID的设置。EDID是一种VESA 标准数据格式,其中包含有关监视器及其性能的参数,包括供应商信息、最大图像大小、颜色设置、厂商预设置、频率范围的限制以及显示器名和序列号的字符串。我们不去分析它了。
337行再次执行store_mode_params()来设置boot_params的screen_info字段,最后根据是否进入了mode_menu设置了do_restore来恢复刚刚被保存的screen_info信息,它跟我们刚刚介绍过的save_screen正好相反:
254static void restore_screen(void) 255{ 256 /* Should be called after store_mode_params() */ 257 int xs = boot_params.screen_info.orig_video_cols; 258 int ys = boot_params.screen_info.orig_video_lines; 259 int y; 260 addr_t dst = 0; 261 u16 *src = saved.data; 262 struct biosregs ireg; 263 264 if (graphic_mode) 265 return; /* Can't restore onto a graphic mode */ 266 267 if (!src) 268 return; /* No saved screen contents */ 269 270 /* Restore screen contents */ 271 272 set_fs(video_segment); 273 for (y = 0; y < ys; y++) { 274 int npad; 275 276 if (y < saved.y) { 277 int copy = (xs < saved.x) ? xs : saved.x; 278 copy_to_fs(dst, src, copy*sizeof(u16)); 279 dst += copy*sizeof(u16); 280 src += saved.x; 281 npad = (xs < saved.x) ? 0 : xs-saved.x; 282 } else { 283 npad = xs; 284 } 285 286 /* Writes "npad" blank characters to 287 video_segment:dst and advances dst */ 288 asm volatile("pushw %%es ; " 289 "movw %2,%%es ; " 290 "shrw %%cx ; " 291 "jnc 1f ; " 292 "stosw /n/t" 293 "1: rep;stosl ; " 294 "popw %%es" 295 : "+D" (dst), "+c" (npad) 296 : "bdS" (video_segment), 297 "a" (0x07200720)); 298 } 299 300 /* Restore cursor position */ 301 if (saved.curx >= xs) 302 saved.curx = xs-1; 303 if (saved.cury >= ys) 304 saved.cury = ys-1; 305 306 initregs(&ireg); 307 ireg.ah = 0x02; /* Set cursor position */ 308 ireg.dh = saved.cury; 309 ireg.dl = saved.curx; 310 intcall(0x10, &ireg, NULL); 311 312 store_cursor_position(); 313} |
当然,针对我们的grub,也是不会走这一步的,所以上面的代码也就只是摆设而已了。