上一篇博文我们讲了如何看到实验结果,这篇博文我们着重分析源代码。
书中作者为了说明原理,约定了一种比较简单地用户程序头部格式,示意图如下(我参考原书图8-15绘制的,左边的数字表示偏移地址):

所以,如果用户程序要利用本章的源码c08_mbr.asm生成的加载器来加载的话,就应该遵循这种头部格式。

下面我们讲解源码c08_mbr.asm(粘贴的源代码不一定和配书的代码完全一样,因为有些地方我加了注释)

<code class="hljs avrasm has-numbering">           <span class="hljs-comment">;代码清单8-1</span><span class="hljs-comment">;文件名:c08_mbr.asm</span><span class="hljs-comment">;文件说明:硬盘主引导扇区代码(加载程序) </span><span class="hljs-comment">;创建日期:2011-5-5 18:17</span>app_lba_start equ <span class="hljs-number">100</span>           <span class="hljs-comment">;声明常数(用户程序起始逻辑扇区号)</span><span class="hljs-comment">;常数的声明不会占用汇编地址</span>SECTION mbr align=<span class="hljs-number">16</span> vstart=<span class="hljs-number">0x7c00</span>                                     <span class="hljs-comment">;设置堆栈段和栈指针 </span><span class="hljs-keyword">mov</span> ax,<span class="hljs-number">0</span>      <span class="hljs-keyword">mov</span> ss,ax<span class="hljs-keyword">mov</span> sp,ax<span class="hljs-keyword">mov</span> ax,[cs:phy_base]            <span class="hljs-comment">;计算用于加载用户程序的逻辑段地址 </span><span class="hljs-keyword">mov</span> dx,[cs:phy_base+<span class="hljs-number">0x02</span>]<span class="hljs-keyword">mov</span> bx,<span class="hljs-number">16</span>        div bx            <span class="hljs-keyword">mov</span> ds,ax                       <span class="hljs-comment">;令DS和ES指向该段以进行操作</span><span class="hljs-keyword">mov</span> es,ax                        <span class="hljs-comment">;以下读取程序的起始部分 </span>xor di,di<span class="hljs-keyword">mov</span> si,app_lba_start            <span class="hljs-comment">;程序在硬盘上的起始逻辑扇区号 </span>xor bx,bx                       <span class="hljs-comment">;加载到DS:0x0000处 </span><span class="hljs-keyword">call</span> read_hard_disk_0<span class="hljs-comment">;以下判断整个程序有多大</span><span class="hljs-keyword">mov</span> dx,[<span class="hljs-number">2</span>]                      <span class="hljs-comment">;曾经把dx写成了ds,花了二十分钟排错 </span><span class="hljs-keyword">mov</span> ax,[<span class="hljs-number">0</span>]<span class="hljs-keyword">mov</span> bx,<span class="hljs-number">512</span>                      <span class="hljs-comment">;512字节每扇区</span>div bxcmp dx,<span class="hljs-number">0</span>jnz <span class="hljs-localvars">@1</span>                          <span class="hljs-comment">;未除尽,因此结果比实际扇区数少1 </span><span class="hljs-keyword">dec</span> ax                          <span class="hljs-comment">;已经读了一个扇区,扇区总数减1 </span><span class="hljs-localvars">@1</span>:cmp ax,<span class="hljs-number">0</span>                        <span class="hljs-comment">;考虑实际长度小于等于512个字节的情况 </span>jz direct<span class="hljs-comment">;读取剩余的扇区</span><span class="hljs-keyword">push</span> ds                         <span class="hljs-comment">;以下要用到并改变DS寄存器 </span><span class="hljs-keyword">mov</span> cx,ax                       <span class="hljs-comment">;循环次数(剩余扇区数)</span><span class="hljs-localvars">@2</span>:<span class="hljs-keyword">mov</span> ax,ds<span class="hljs-keyword">add</span> ax,<span class="hljs-number">0x20</span>                     <span class="hljs-comment">;得到下一个以512字节为边界的段地址</span><span class="hljs-keyword">mov</span> ds,ax  xor bx,bx                       <span class="hljs-comment">;每次读时,偏移地址始终为0x0000 </span><span class="hljs-keyword">inc</span> si                          <span class="hljs-comment">;下一个逻辑扇区 </span><span class="hljs-keyword">call</span> read_hard_disk_0loop <span class="hljs-localvars">@2</span>                         <span class="hljs-comment">;循环读,直到读完整个功能程序 </span><span class="hljs-keyword">pop</span> ds                          <span class="hljs-comment">;恢复数据段基址到用户程序头部段 </span><span class="hljs-comment">;计算入口点代码段基址 </span>direct:<span class="hljs-keyword">mov</span> dx,[<span class="hljs-number">0x08</span>]<span class="hljs-keyword">mov</span> ax,[<span class="hljs-number">0x06</span>]<span class="hljs-keyword">call</span> calc_segment_base<span class="hljs-keyword">mov</span> [<span class="hljs-number">0x06</span>],ax                   <span class="hljs-comment">;回填修正后的入口点代码段基址 </span><span class="hljs-comment">;开始处理段重定位表</span><span class="hljs-keyword">mov</span> cx,[<span class="hljs-number">0x0a</span>]                   <span class="hljs-comment">;需要重定位的项目数量</span><span class="hljs-keyword">mov</span> bx,<span class="hljs-number">0x0c</span>                     <span class="hljs-comment">;重定位表首地址</span>realloc:<span class="hljs-keyword">mov</span> dx,[bx+<span class="hljs-number">0x02</span>]                <span class="hljs-comment">;32位地址的高16位 </span><span class="hljs-keyword">mov</span> ax,[bx]<span class="hljs-keyword">call</span> calc_segment_base<span class="hljs-keyword">mov</span> [bx],ax                     <span class="hljs-comment">;回填段的基址</span><span class="hljs-keyword">add</span> bx,<span class="hljs-number">4</span>                        <span class="hljs-comment">;下一个重定位项(每项占4个字节) </span>loop realloc <span class="hljs-keyword">jmp</span> far [<span class="hljs-number">0x04</span>]                  <span class="hljs-comment">;转移到用户程序  </span><span class="hljs-comment">;-------------------------------------------------------------------------------</span>
<span class="hljs-label">read_hard_disk_0:</span>                        <span class="hljs-comment">;从硬盘读取一个逻辑扇区</span><span class="hljs-comment">;输入:DI:SI=起始逻辑扇区号</span><span class="hljs-comment">;      DS:BX=目标缓冲区地址</span><span class="hljs-keyword">push</span> ax<span class="hljs-keyword">push</span> bx<span class="hljs-keyword">push</span> cx<span class="hljs-keyword">push</span> dx<span class="hljs-keyword">mov</span> dx,<span class="hljs-number">0x1f2</span><span class="hljs-keyword">mov</span> al,<span class="hljs-number">1</span><span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;读取的扇区数</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f3</span><span class="hljs-keyword">mov</span> ax,si<span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;LBA地址7~0</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f4</span><span class="hljs-keyword">mov</span> al,ah<span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;LBA地址15~8</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f5</span><span class="hljs-keyword">mov</span> ax,di<span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;LBA地址23~16</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f6</span><span class="hljs-keyword">mov</span> al,<span class="hljs-number">0xe0</span>                     <span class="hljs-comment">;LBA28模式,主盘</span><span class="hljs-keyword">or</span> al,ah                        <span class="hljs-comment">;LBA地址27~24</span><span class="hljs-keyword">out</span> dx,al<span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f7</span><span class="hljs-keyword">mov</span> al,<span class="hljs-number">0x20</span>                     <span class="hljs-comment">;读命令</span><span class="hljs-keyword">out</span> dx,al<span class="hljs-preprocessor">.waits</span>:<span class="hljs-keyword">in</span> al,dx<span class="hljs-keyword">and</span> al,<span class="hljs-number">0x88</span>cmp al,<span class="hljs-number">0x08</span>jnz <span class="hljs-preprocessor">.waits</span>                      <span class="hljs-comment">;不忙,且硬盘已准备好数据传输 </span><span class="hljs-keyword">mov</span> cx,<span class="hljs-number">256</span>                      <span class="hljs-comment">;总共要读取的字数</span><span class="hljs-keyword">mov</span> dx,<span class="hljs-number">0x1f0</span><span class="hljs-preprocessor">.readw</span>:<span class="hljs-keyword">in</span> ax,dx<span class="hljs-keyword">mov</span> [bx],ax<span class="hljs-keyword">add</span> bx,<span class="hljs-number">2</span>loop <span class="hljs-preprocessor">.readw</span><span class="hljs-keyword">pop</span> dx<span class="hljs-keyword">pop</span> cx<span class="hljs-keyword">pop</span> bx<span class="hljs-keyword">pop</span> ax<span class="hljs-keyword">ret</span><span class="hljs-comment">;-------------------------------------------------------------------------------</span>
<span class="hljs-label">calc_segment_base:</span>                       <span class="hljs-comment">;计算16位段地址</span><span class="hljs-comment">;输入:DX:AX=32位物理地址</span><span class="hljs-comment">;返回:AX=16位段基地址 </span><span class="hljs-keyword">push</span> dx                          <span class="hljs-keyword">add</span> ax,[cs:phy_base]<span class="hljs-keyword">adc</span> dx,[cs:phy_base+<span class="hljs-number">0x02</span>]shr ax,<span class="hljs-number">4</span><span class="hljs-keyword">ror</span> dx,<span class="hljs-number">4</span><span class="hljs-keyword">and</span> dx,<span class="hljs-number">0xf000</span><span class="hljs-keyword">or</span> ax,dx<span class="hljs-keyword">pop</span> dx<span class="hljs-keyword">ret</span><span class="hljs-comment">;-------------------------------------------------------------------------------</span>phy_base dd <span class="hljs-number">0x10000</span>             <span class="hljs-comment">;用户程序被加载的物理起始地址</span>times <span class="hljs-number">510</span>-($-$$) db <span class="hljs-number">0</span>db <span class="hljs-number">0x55</span>,<span class="hljs-number">0xaa</span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li><li>56</li><li>57</li><li>58</li><li>59</li><li>60</li><li>61</li><li>62</li><li>63</li><li>64</li><li>65</li><li>66</li><li>67</li><li>68</li><li>69</li><li>70</li><li>71</li><li>72</li><li>73</li><li>74</li><li>75</li><li>76</li><li>77</li><li>78</li><li>79</li><li>80</li><li>81</li><li>82</li><li>83</li><li>84</li><li>85</li><li>86</li><li>87</li><li>88</li><li>89</li><li>90</li><li>91</li><li>92</li><li>93</li><li>94</li><li>95</li><li>96</li><li>97</li><li>98</li><li>99</li><li>100</li><li>101</li><li>102</li><li>103</li><li>104</li><li>105</li><li>106</li><li>107</li><li>108</li><li>109</li><li>110</li><li>111</li><li>112</li><li>113</li><li>114</li><li>115</li><li>116</li><li>117</li><li>118</li><li>119</li><li>120</li><li>121</li><li>122</li><li>123</li><li>124</li><li>125</li><li>126</li><li>127</li><li>128</li><li>129</li><li>130</li><li>131</li><li>132</li><li>133</li><li>134</li><li>135</li><li>136</li><li>137</li><li>138</li><li>139</li><li>140</li><li>141</li><li>142</li><li>143</li><li>144</li><li>145</li><li>146</li><li>147</li><li>148</li><li>149</li><li>150</li><li>151</li><li>152</li><li>153</li><li>154</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li><li>56</li><li>57</li><li>58</li><li>59</li><li>60</li><li>61</li><li>62</li><li>63</li><li>64</li><li>65</li><li>66</li><li>67</li><li>68</li><li>69</li><li>70</li><li>71</li><li>72</li><li>73</li><li>74</li><li>75</li><li>76</li><li>77</li><li>78</li><li>79</li><li>80</li><li>81</li><li>82</li><li>83</li><li>84</li><li>85</li><li>86</li><li>87</li><li>88</li><li>89</li><li>90</li><li>91</li><li>92</li><li>93</li><li>94</li><li>95</li><li>96</li><li>97</li><li>98</li><li>99</li><li>100</li><li>101</li><li>102</li><li>103</li><li>104</li><li>105</li><li>106</li><li>107</li><li>108</li><li>109</li><li>110</li><li>111</li><li>112</li><li>113</li><li>114</li><li>115</li><li>116</li><li>117</li><li>118</li><li>119</li><li>120</li><li>121</li><li>122</li><li>123</li><li>124</li><li>125</li><li>126</li><li>127</li><li>128</li><li>129</li><li>130</li><li>131</li><li>132</li><li>133</li><li>134</li><li>135</li><li>136</li><li>137</li><li>138</li><li>139</li><li>140</li><li>141</li><li>142</li><li>143</li><li>144</li><li>145</li><li>146</li><li>147</li><li>148</li><li>149</li><li>150</li><li>151</li><li>152</li><li>153</li><li>154</li></ul>

app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
这句话作者假定用户程序从硬盘第100扇区开始。所以在我们把这个源文件对应的.bin文件写入虚拟硬盘的时候,要从逻辑扇区100开始写。

equ 类似于C语言中的#define,用来定义一个常量。
一般使用格式:
符号名 EQU 表达式
作用是左边的符号名代表右边的表达式。
注意:不会给符号名分配存储空间,符号名不能与其它符号同名,也不能被重新定义

SECTION mbr align=16 vstart=0x7c00
解释:
NASM编译器用SECTION或者SEGMENT来定义段。mbr是段名称(可以随便起);
注意:如果整个程序都没有段定义语句,那么整个程序自成一个段(这点好像和MASM不同哦!);
align=16 表示16字节对齐;
vstart=0x7c00,关于这个,我们就不得不多说几句了。

==================插叙部分================
汇编地址以及标号的本质:
1. 所谓汇编地址,就是编译器给源程序中每条指令定义的地址,由于编译后的程序可以在内存中浮动(即可以装载在内存中的任意位置),因此直接用绝对地址(20位的实模式下的物理内存地址)来给源程序中的指令定位的话将不利于程序在内存中的浮动;
2. 汇编地址定位规则:
(1)一般规则:
i. 如果在没有使用特殊指令的一般情况下(特别是vstart指令),整个源程序中第一条指令的汇编地址为0,之后所有指令的汇编地址都是相对于整个源程序第一条指令的偏移地址,即使程序中分了很多段也是如此。在这种情况下,如果将整个源程序看做一个段的话则汇编地址就是段内偏移地址;
ii. 在NASM中,所有的标号实质上就是其所在处指令的汇编地址,在编译后会将所有标号都替换成该汇编地址值(即立即数);
(2)特殊规则:
i. 如果在定义段的时候使用了vstart伪指令,比如
“section my_segment vstart=15”,
则会提醒汇编器,该段起始指令的汇编地址是15,段内的其它指令的汇编地址都是距该段起始指令地址的偏移量加上15;因此,vstart伪指令就是指定段的起始汇编地址;如果vstart=0,则段内的汇编地址就是段内的偏移地址!(这种手法经常使用!)
ii. 使用NASM规则的标准段,是指section .data、section .text、section .bss,这三种标准段都默认包含有vstart=0,因此段内的指令以及标号的汇编地址都是段内偏移地址,并且在加载程序的时候会自动使cs指向.text,ds指向.bss,es指向.data,而无需人手工执行对段寄存器赋值的步骤,而对于i.中的定义段的方式则没有这种自动的步骤,需要亲手对段寄存器进行赋值(是这样吗?从网上搜来的,我不能肯定。)
(3) 引用标号:
i. 和MASM不一样的是NASM大大简化了对标号的引用,不需要再用seg和offset对标号取段地址和偏移地址了;
ii. 在NASM中,标号就是一个立即数,而这个立即数就是汇编地址;
iii. 在NASM中不再有MASM中数据标号的概念,也就不存在什么arr[5]之类的内存寻址形式了!
iv. 在NASM中所有出现标号的地方都会用标号的汇编地址替换,因此诸如mov ax, tag之类的指令,仅仅就是将一个立即数(tag的汇编地址)传送至ax而已,而不是取tag地址内的数据了!如果要取标号处内存中的数据就必须使用[ ](类似C语言中的指针运算符*);
==================插叙结束================

处理器加电或者复位后,BIOS会执行硬件检测和初始化程序,如果没有错误,接下来就会进行操作系统引导。
BIOS会根据CMOS(一块可读写的RAM芯片,保存系统当前的硬件配置和用户的设定参数)里记录的启动顺序逐个地来尝试加载启动代码。
具体的过程是BIOS将磁盘的第一扇区(磁盘最开始的512字节,也就是主引导扇区)载入内存,放在0X0000:0X7C00处,然后检查这个扇区的最后两个字节是不是“0x55AA”,如果是则认为这是一个有效的启动扇区,如果不是就会尝试下一个启动介质;
如果主引导扇区有效,则以一个段间转移指令
jmp 0x0000:0x7c00
跳过去继续执行;
如果所有的启动介质都判断过后仍然没有找到可启动的程序,那么BIOS会给出错误提示。

所以,代码中的vstart=0x7c00不是空穴来风,而是根据代码被加载的实际位置决定的。

当这段程序刚被加载到内存后,
CS=0x0000, IP=0x7c00

如上图所示,假设不写vstart=0x7c00,那么标号“number”的偏移地址就从程序头(认为是0)开始算起,为0x012e;
但是实际上“number”的段内偏移地址是0x7d2e(0x012e+0x7c00=0x7d2e)!
为了修正这个偏移地址的差值,于是有vstart=0x7c00,也就是说段内所有指令的汇编地址都在原来的基础上加上0x7c00.

这里还要再补充一点,如果看这个源文件对应的列表文件,是看不出来偏移地址被加了0x7c00的。
列表文件的一个截图如下:

看到了吗?第一条指令的汇编地址,还是从0开始的!
而且
SECTION mbr align=16 vstart=0x7c00
这句话还是出现在了列表文件里。
我的理解是,列表文件仅仅是对源码的第一遍扫描吧。在后面的扫描中,0x7c00就起作用了。

举个例子吧,
上图有一行
16 00000007 2EA1[CA00] mov ax,[cs:phy_base]

列表文件的末尾有
151 000000CA 00000100 phy_base dd 0x10000

也就是说 phy_base 这个标号的汇编地址就是00CA(这时候7C00还没有起作用)

我们再看一下编译后的二进制文件

在偏移为0x07的地方,对应的指令码是
2EA1CA7C
注意到其中的CA7C(低字节在前面)了吗? 这个就是00CA+7C00=7CCA的结果啊!

我们继续看代码,
;设置堆栈段和栈指针
mov ax,0
mov ss,ax
mov sp,ax
定义栈需要两个连续的步骤,即初始化SS和SP.

*——————-小贴士—————-
原书P158上方:处理器在设计的时候就规定,当遇到修改段寄存器SS的指令时,在这条指令和下一条指令执行完毕期间,禁止中断,以此来保护栈。也就是说,我们应该在修改SS的指令之后,紧接着一条修改SP的指令。
——————————————–*
因为已经设置了SP=SS=0,所以第一次执行PUSH指令时,先把SP减2,即0x0000-0x000=0xFFFE(借位被忽略);然后把内容送入SS:SP指向的内存单元处。如下图所示(文章中画的只是示意图,不是按照比例画的,凑合看)

     mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 mov dx,[cs:phy_base+0x02]mov bx,16        div bx            mov ds,ax                       ;令DS和ES指向该段以进行操作mov es,ax

代码的末尾部分有
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
也就是说作者安排把用户程序加载到物理内存0x10000处,(我们完全可以修改成别的16字节对齐的地址,只要把用户程序加载到一个空闲的地方就可以。)
上面这几行的意思是根据物理地址计算出逻辑段地址,[DX:AX]是被除数,BX的内容是除数(16),计算结果在AX(对于本程序,结果就是0x1000)中。然后令DS和ES都指向这个段。

;以下读取程序的起始部分 xor di,dimov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 xor bx,bx                       ;加载到DS:0x0000处 call read_hard_disk_0

这段代码的最后调用了过程 read_hard_disk_0,我们看一下过程调用的代码,我在代码中加了一些注释:

<code class="hljs avrasm has-numbering"><span class="hljs-label">read_hard_disk_0:</span>              <span class="hljs-comment">;从硬盘读取一个逻辑扇区</span><span class="hljs-comment">;输入:DI:SI=起始逻辑扇区号</span><span class="hljs-comment">; DS:BX=目标缓冲区地址</span><span class="hljs-comment">;使用LBA28寻址方式</span><span class="hljs-keyword">push</span> ax<span class="hljs-keyword">push</span> bx<span class="hljs-keyword">push</span> cx<span class="hljs-keyword">push</span> dx<span class="hljs-comment">; 用到的寄存器压栈保存</span><span class="hljs-keyword">mov</span> dx,<span class="hljs-number">0x1f2</span><span class="hljs-keyword">mov</span> al,<span class="hljs-number">1</span><span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;读取的扇区数为1</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f3</span><span class="hljs-keyword">mov</span> ax,si<span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;LBA地址7~0</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f4</span><span class="hljs-keyword">mov</span> al,ah<span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;LBA地址15~8</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f5</span><span class="hljs-keyword">mov</span> ax,di<span class="hljs-keyword">out</span> dx,al                       <span class="hljs-comment">;LBA地址23~16</span><span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f6</span><span class="hljs-keyword">mov</span> al,<span class="hljs-number">0xe0</span>                     <span class="hljs-comment">;LBA28模式,主盘</span><span class="hljs-keyword">or</span> al,ah                        <span class="hljs-comment">;LBA地址27~24</span><span class="hljs-keyword">out</span> dx,al<span class="hljs-keyword">inc</span> dx                          <span class="hljs-comment">;0x1f7</span><span class="hljs-keyword">mov</span> al,<span class="hljs-number">0x20</span>                     <span class="hljs-comment">;读命令</span><span class="hljs-keyword">out</span> dx,al<span class="hljs-preprocessor">.waits</span>:<span class="hljs-keyword">in</span> al,dx<span class="hljs-keyword">and</span> al,<span class="hljs-number">0x88</span>cmp al,<span class="hljs-number">0x08</span>jnz <span class="hljs-preprocessor">.waits</span>                      <span class="hljs-comment">;不忙,且硬盘已准备好数据传输 </span><span class="hljs-keyword">mov</span> cx,<span class="hljs-number">256</span>                      <span class="hljs-comment">;总共要读取的字数</span><span class="hljs-keyword">mov</span> dx,<span class="hljs-number">0x1f0</span><span class="hljs-preprocessor">.readw</span>:<span class="hljs-keyword">in</span> ax,dx<span class="hljs-keyword">mov</span> [bx],ax<span class="hljs-keyword">add</span> bx,<span class="hljs-number">2</span>loop <span class="hljs-preprocessor">.readw</span><span class="hljs-keyword">pop</span> dx<span class="hljs-keyword">pop</span> cx<span class="hljs-keyword">pop</span> bx<span class="hljs-keyword">pop</span> ax<span class="hljs-keyword">ret</span>
</code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li></ul>

要理解这段,先看下面的示意图(参照原书图8-11画的)
inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al

mov al,0xe0 表示选择LBA模式,选择主硬盘
注意,在调用这个过程的时候,DI:SI=起始逻辑扇区号,DI的低四位是有效的,高四位应该为0,其实这里我觉得应该加一句,
mov al,0xe0 这句后面加一句 and ah,0x0f
目的是把DI的高四位清零,万一调用者忘记清零了,这样做可以防止意外发生。

     inc dx                          ;0x1f7mov al,0x20                     ;读命令out dx,al

当把起始LBA扇区号设置好后,就可以发出读命令了。上面的代码表示向端口0x1F7写入0x20,请求读硬盘。
接下来等待读请求完成。端口0x1F7既是命令端口,也是状态端口。部分状态位的含义如图:

<code class="hljs avrasm has-numbering">  <span class="hljs-preprocessor">.waits</span>:<span class="hljs-keyword">in</span> al,dx      <span class="hljs-comment">;读端口的值</span><span class="hljs-keyword">and</span> al,<span class="hljs-number">0x88</span>   <span class="hljs-comment">;提取出bit3和bit7</span>cmp al,<span class="hljs-number">0x08</span>   <span class="hljs-comment">;bit3==1且bit7==0说明准备好了</span>jnz <span class="hljs-preprocessor">.waits</span>    <span class="hljs-comment">;否则继续检查 </span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>

一旦硬盘准备好了,就可以读取数据了。0x1F0是硬盘接口的数据端口,是16位的。可以连续从这个端口读取数据。
mov cx,256
in ax,dx
这两句话就表示读取了一个字的数据(16位)到AX中

<code class="hljs avrasm has-numbering">         <span class="hljs-keyword">mov</span> cx,<span class="hljs-number">256</span>              <span class="hljs-comment">;总共要读取的字数</span><span class="hljs-keyword">mov</span> dx,<span class="hljs-number">0x1f0</span><span class="hljs-preprocessor">.readw</span>:<span class="hljs-keyword">in</span> ax,dx<span class="hljs-keyword">mov</span> [bx],ax   <span class="hljs-comment">;读取的数据放在数据段,偏移地址由BX指定</span><span class="hljs-keyword">add</span> bx,<span class="hljs-number">2</span>loop <span class="hljs-preprocessor">.readw</span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul>

现在我们再回到那部分代码,就很容易理解了。

     xor di,di   ;di清零 (因为我们传入逻辑扇区号是100,不超过16 bits)mov si,app_lba_start   ;程序在硬盘上的起始逻辑扇区号 xor bx,bx              ;加载到DS:0x0000处 call read_hard_disk_0

执行到这里,内存大概如下图所示:

<code class="hljs avrasm has-numbering">  <span class="hljs-comment">;以下判断整个程序有多大</span><span class="hljs-keyword">mov</span> dx,[<span class="hljs-number">2</span>]                      <span class="hljs-comment">;曾经把dx写成了ds,花了二十分钟排错 </span><span class="hljs-keyword">mov</span> ax,[<span class="hljs-number">0</span>]<span class="hljs-keyword">mov</span> bx,<span class="hljs-number">512</span>                      <span class="hljs-comment">;512字节每扇区</span>div bxcmp dx,<span class="hljs-number">0</span>jnz <span class="hljs-localvars">@1</span>                          <span class="hljs-comment">;未除尽,因此结果比实际扇区数少1 </span><span class="hljs-keyword">dec</span> ax                          <span class="hljs-comment">;已经读了一个扇区,扇区总数减1 </span><span class="hljs-localvars">@1</span>:cmp ax,<span class="hljs-number">0</span>                        <span class="hljs-comment">;考虑实际长度小于等于512个字节的情况 </span>jz direct<span class="hljs-comment">;读取剩余的扇区</span><span class="hljs-keyword">push</span> ds                         <span class="hljs-comment">;以下要用到并改变DS寄存器 </span><span class="hljs-keyword">mov</span> cx,ax                       <span class="hljs-comment">;循环次数(剩余扇区数)</span><span class="hljs-localvars">@2</span>:<span class="hljs-keyword">mov</span> ax,ds<span class="hljs-keyword">add</span> ax,<span class="hljs-number">0x20</span>                     <span class="hljs-comment">;得到下一个以512字节为边界的段地址</span><span class="hljs-keyword">mov</span> ds,ax  xor bx,bx                       <span class="hljs-comment">;每次读时,偏移地址始终为0x0000 </span><span class="hljs-keyword">inc</span> si                          <span class="hljs-comment">;下一个逻辑扇区 </span><span class="hljs-keyword">call</span> read_hard_disk_0loop <span class="hljs-localvars">@2</span>                         <span class="hljs-comment">;循环读,直到读完整个功能程序 </span><span class="hljs-keyword">pop</span> ds                          <span class="hljs-comment">;恢复数据段基址到用户程序头部段 </span>
</code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li></ul>

上面这段代码是为了把剩余的用户程序读到内存里(以扇区为单位)
我们分别讲解。

<code class="hljs css has-numbering">  ;以下判断整个程序有多大<span class="hljs-tag">mov</span> <span class="hljs-tag">dx</span>,<span class="hljs-attr_selector">[2]</span>                     <span class="hljs-tag">mov</span> <span class="hljs-tag">ax</span>,<span class="hljs-attr_selector">[0]</span><span class="hljs-tag">mov</span> <span class="hljs-tag">bx</span>,512                      ;512字节每扇区<span class="hljs-tag">div</span> <span class="hljs-tag">bx</span><span class="hljs-tag">cmp</span> <span class="hljs-tag">dx</span>,0<span class="hljs-tag">jnz</span> <span class="hljs-at_rule">@<span class="hljs-keyword">1</span>                          </span>;未除尽,因此结果比实际扇区数少1 <span class="hljs-tag">dec</span> <span class="hljs-tag">ax</span>                          ;已经读了一个扇区,扇区总数减1 <span class="hljs-at_rule">@<span class="hljs-keyword">1:</span>cmp ax,<span class="hljs-number">0</span>                        </span>;考虑实际长度小于等于512个字节的情况 <span class="hljs-tag">jz</span> <span class="hljs-tag">direct</span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li></ul>

因为已经约定了用户程序的头部4个字节是用户程序的总长度,所以这里取总长度到[dx:ax]中,把[dx:ax]除以512,就能得到有几个扇区。dx存放余数,ax存放商。
如果dx==0,那么就把ax减一(因为前面已经读了一个扇区),继续执行@1;如果dx!=0,那么剩余的扇区数就是ax,然后跳到@1;
开始执行@1处的代码时,ax已经保存了还要读取的扇区数,但是这个值也有可能为0,如果为0,就不用再读取了, jz direct就可以;如果不为0,就执行下面的代码。
好了,如果你觉得上面说得不够清楚,那么看这个简单的流程图吧:

<code class="hljs avrasm has-numbering">  <span class="hljs-comment">;读取剩余的扇区</span><span class="hljs-keyword">push</span> ds                         <span class="hljs-comment">;以下要用到并改变DS寄存器 </span><span class="hljs-keyword">mov</span> cx,ax                       <span class="hljs-comment">;循环次数(剩余扇区数)</span><span class="hljs-localvars">@2</span>:<span class="hljs-keyword">mov</span> ax,ds<span class="hljs-keyword">add</span> ax,<span class="hljs-number">0x20</span>                     <span class="hljs-comment">;得到下一个以512字节为边界的段地址</span><span class="hljs-keyword">mov</span> ds,ax  xor bx,bx                       <span class="hljs-comment">;每次读时,偏移地址始终为0x0000 </span><span class="hljs-keyword">inc</span> si                          <span class="hljs-comment">;下一个逻辑扇区 </span><span class="hljs-keyword">call</span> read_hard_disk_0loop <span class="hljs-localvars">@2</span>                         <span class="hljs-comment">;循环读,直到读完整个功能程序 </span><span class="hljs-keyword">pop</span> ds                          <span class="hljs-comment">;恢复数据段基址到用户程序头部段 </span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li></ul>

mov ax,ds
add ax,0x20
mov ds,ax ;这三行表示调整ds的位置,让ds指向最后读入的块的末尾,也就是将要读入的块的开始。其他语句都好理解,这里就不解释了。
接下来是处理段的重定位表。我们要修正每个表项的值。
为什么要修正呢?看图就明白了。

用户程序在编译的时候,每个段的段地址都是相对于程序开头(0)计算的。但是用户程序被加载器加到到物理地址[phy_base]的时候,相当于每个段的物理地址都向后偏移了[phy_base],所以我们要修正这个差值。
我们看看代码:

<code class="hljs avrasm has-numbering"><span class="hljs-label">calc_segment_base:</span>               <span class="hljs-comment">;计算16位段地址</span><span class="hljs-comment">;输入:DX:AX=32位物理地址</span><span class="hljs-comment">;返回:AX=16位段基地址 </span><span class="hljs-keyword">push</span> dx                          <span class="hljs-keyword">add</span> ax,[cs:phy_base]<span class="hljs-keyword">adc</span> dx,[cs:phy_base+<span class="hljs-number">0x02</span>]shr ax,<span class="hljs-number">4</span><span class="hljs-keyword">ror</span> dx,<span class="hljs-number">4</span><span class="hljs-keyword">and</span> dx,<span class="hljs-number">0xf000</span><span class="hljs-keyword">or</span> ax,dx<span class="hljs-keyword">pop</span> dx<span class="hljs-keyword">ret</span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li></ul>

add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02];
这两句其实是做了一个20位数的加法,修正后的物理地址是[dx:ax];
shr ax,4
ror dx,4
and dx,0xf000
or ax,dx;
这四句是求出段基地址(16位),也就是逻辑段地址,结果在AX中。然后回填到原处(仅覆盖低16位,高16位不用管)。
为什么要求出段基地址呢?因为在用户程序中,对段寄存器赋值,都是从这里引用的。

<code class="hljs avrasm has-numbering"> <span class="hljs-comment">;计算入口点代码段基址 </span>direct:<span class="hljs-keyword">mov</span> dx,[<span class="hljs-number">0x08</span>]<span class="hljs-keyword">mov</span> ax,[<span class="hljs-number">0x06</span>]<span class="hljs-keyword">call</span> calc_segment_base<span class="hljs-keyword">mov</span> [<span class="hljs-number">0x06</span>],ax                   <span class="hljs-comment">;回填修正后的入口点代码段基址 </span><span class="hljs-comment">;开始处理段重定位表</span><span class="hljs-keyword">mov</span> cx,[<span class="hljs-number">0x0a</span>]                   <span class="hljs-comment">;需要重定位的项目数量</span><span class="hljs-keyword">mov</span> bx,<span class="hljs-number">0x0c</span>                     <span class="hljs-comment">;重定位表首地址</span>realloc:<span class="hljs-keyword">mov</span> dx,[bx+<span class="hljs-number">0x02</span>]                <span class="hljs-comment">;32位地址的高16位 </span><span class="hljs-keyword">mov</span> ax,[bx]<span class="hljs-keyword">call</span> calc_segment_base<span class="hljs-keyword">mov</span> [bx],ax                     <span class="hljs-comment">;回填段的基址</span><span class="hljs-keyword">add</span> bx,<span class="hljs-number">4</span>                        <span class="hljs-comment">;下一个重定位项(每项占4个字节) </span>loop realloc <span class="hljs-keyword">jmp</span> far [<span class="hljs-number">0x04</span>]                  <span class="hljs-comment">;转移到用户程序  </span></code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li></ul><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li></ul>

只要参考本文开头的用户程序头部示意图,上面这段代码不难理解。
需要说明的是 jmp far [0x04] ;这个是16位间接绝对远转移指令。一定要使用关键字far。处理器执行这条指令的时候,会访问DS所指向的数据段,从偏移地址0x04处取出两个字(低字是偏移地址,高字是段基址),用低字代替IP的内容,用高字代替CS的内容,于是就可以转移了。

硬盘和显卡的访问与控制(二)——《x86汇编语言:从实模式到保护模式》读书笔记02相关推荐

  1. [书]x86汇编语言:从实模式到保护模式 -- 第八章 硬盘和显卡的访问与控制,mbr加载并重定位应用程序

    第八章 硬盘和显卡的访问与控制 mbr加载.重定位用户程序 PART 1 >> VirtualBox显示最终效果 ===================================== ...

  2. 第八章 硬盘和显卡的访问与控制(2)

    第八章 硬盘和显卡的访问与控制 过程调用 编写过程的好处是只用编写一次,以后只需要调用即可. 8-1的第24~27行用于读取app_lba_start扇区的内容,即用户程序在硬盘上的起始逻辑扇区号. ...

  3. 硬盘和显卡的访问与控制(一)——《x86汇编语言:从实模式到保护模式》读书笔记01

    本文是<x86汇编语言:从实模式到保护模式>(电子工业出版社)的读书实验笔记. 这篇文章我们先不分析代码,而是说一下在Bochs环境下如何看到实验结果. 需要的源码文件 第一个文件是加载程 ...

  4. 硬盘和显卡的访问与控制

    离开主引导扇区,前方是操作系统.         和主引导扇区程序一样,操作系统也位于硬盘是上.操作系统安装到硬盘上,安装过程不但要把操作系统的指令和数据写入硬盘,通常还需更新主引导扇区内容.    ...

  5. 19、硬盘和显卡的访问与控制

    文章目录 01.离开主引导分区 02.给汇编程序分段 03.控制段内元素的汇编地址 04.加载器和用户程序头部段 05.加载器的工作流程和常数声明 06.确定用户程序的加载位置 07.外围设备及其接口 ...

  6. 硬盘和显卡的访问与控制(三)(含多彩的Hello)——《x86汇编语言:从实模式到保护模式》读书笔记03

    上一篇博文我们用了很大的篇幅说了加载器,这一篇我们该说说用户程序了. 先看作者的源码吧. ;代码清单8-2;文件名:c08.asm;文件说明:用户程序 ;创建日期:2011-5-5 18:17;=== ...

  7. 第8章 硬盘和显卡的访问与控制

    首先声明,这一章非常重要,如果刚开始读不懂,读不下去,一定要坚持,还有读这本书的一个要求是王爽<汇编语言>看两遍,并做完所有的课后实验.这一章其实是操作系统的的加载和引导过程.其中涉及的有 ...

  8. 深入理解计算机系统-之-内存寻址(二)--存储保护机制(CPU实模式与保护模式)

    cpu的保护模式由来 分段机制 8086的诞生,标志着Intel 正式进入了x86时代,这是个多么具有纪念意义的日子:1978-6-8.同时,8086的诞生也是处理器内存寻址技术的第一次飞跃. 对于一 ...

  9. 进入保护模式(二)——《x86汇编语言:从实模式到保护模式》读书笔记14

    首先来段题外话:之前我发现我贴出的代码都没有行号,给讲解带来不便.所以从现在起,我要给代码加上行号.我写博客用的这个插入代码的插件,确实不支持自动插入行号.我真的没有找到什么好方法,无奈之下,只能按照 ...

最新文章

  1. php多线程查表,php curl_multi 多线程查询的例子
  2. 神经网络有可能被公式化表达吗?
  3. python画方波_今天学会傅里叶画画,明天就是初音未来 - 如何用Python和Blender画任意图形...
  4. 一句 Task.Result 就死锁, 这代码还怎么写?
  5. cas单点登录系统:客户端(client)详细配置(包含统一单点注销配置)
  6. Alpha冲刺(7/10)
  7. linux中的进程权限是,Linux中权限,进程,服务的简单操作
  8. SpringMVC介绍
  9. bzoj 4260 REBXOR —— Trie树
  10. [android-wifi](7.1)漫游部分逻辑
  11. Lesson 4 Part 1 Newton's method
  12. 地理信息系统(汤国安)重点整理与推导(第二章)
  13. linux关闭firefox进程,Firefox 68+ 怎样关闭多进程
  14. 阿里云短信api 回执 回复
  15. CMD 调用子程序从子程序返回
  16. SEO重要的是初心不变
  17. 安卓多渠道打包(三)360加固多渠道打包
  18. 踩坑 微信小程序开发mpvue使用iconfont,顺便解决偶现图标显示不正确
  19. 美国大学计算机工程专业排名,2018美国大学计算机工程专业排名_美国大学计算机工程排名...
  20. 初探RabbitMQ与简单实现

热门文章

  1. hdu4370 比较抽象的最短路
  2. POJ3277 线段树段更新,点询问+二分离散化+暴力
  3. C语言经典例84-一个偶数总能表示为两个素数之和
  4. 【Windows 逆向】OD 调试器工具 ( 推荐汉化版的 OD 调试工具 | 吾爱破解专用版Ollydbg | 备选工具 )
  5. 【Java 虚拟机原理】Class 字节码二进制文件分析 四 ( 字段表数据结构 | 字段表详细分析 | 访问标志 | 字段名称 | 字段描述符 | 属性项目 )
  6. 第4周小组作业:WordCount优化
  7. ASP.NET Core 新核心对象WebHost(一)
  8. 快速排序及优化(Java实现)
  9. java学习笔记—国际化(41)
  10. java多态性详解——父类引用子类对象