上一篇博文我们讲了如何看到实验结果,这篇博文我们着重分析源代码。

书中作者为了说明原理,约定了一种比较简单地用户程序头部格式,示意图如下(我参考原书图8-15绘制的,左边的数字表示偏移地址):



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

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

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

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指向的内存单元处。如下图所示(文章中画的只是示意图,不是按照比例画的,凑合看)

  1. mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址
  2. mov dx,[cs:phy_base+0x02]
  3. mov bx,16
  4. div bx
  5. mov ds,ax ;令DSES指向该段以进行操作
  6. mov es,ax

代码的末尾部分有

phy_base dd 0x10000 ;用户程序被加载的物理起始地址

也就是说作者安排把用户程序加载到物理内存0x10000处,(我们完全可以修改成别的16字节对齐的地址,只要把用户程序加载到一个空闲的地方就可以。)

上面这几行的意思是根据物理地址计算出逻辑段地址,[DX:AX]是被除数,BX的内容是除数(16),计算结果在AX(对于本程序,结果就是0x1000)中。然后令DS和ES都指向这个段。

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

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

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

要理解这段,先看下面的示意图(参照原书图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的高四位清零,万一调用者忘记清零了,这样做可以防止意外发生。

  1. inc dx ;0x1f7
  2. mov al,0x20 ;读命令
  3. out dx,al

当把起始LBA扇区号设置好后,就可以发出读命令了。上面的代码表示向端口0x1F7写入0x20,请求读硬盘。

接下来等待读请求完成。端口0x1F7既是命令端口,也是状态端口。部分状态位的含义如图:

  1. .waits:
  2. in al,dx ;读端口的值
  3. and al,0x88 ;提取出bit3bit7
  4. cmp al,0x08 ;bit3==1bit7==0说明准备好了
  5. jnz .waits ;否则继续检查

一旦硬盘准备好了,就可以读取数据了。0x1F0是硬盘接口的数据端口,是16位的。可以连续从这个端口读取数据。

mov cx,256

in ax,dx

这两句话就表示读取了一个字的数据(16位)到AX中

  1. mov cx,256 ;总共要读取的字数
  2. mov dx,0x1f0
  3. .readw:
  4. in ax,dx
  5. mov [bx],ax ;读取的数据放在数据段,偏移地址由BX指定
  6. add bx,2
  7. loop .readw

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

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

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

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

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

我们分别讲解。

  1. ;以下判断整个程序有多大
  2. mov dx,[2]
  3. mov ax,[0]
  4. mov bx,512 ;512字节每扇区
  5. div bx
  6. cmp dx,0
  7. jnz @1 ;未除尽,因此结果比实际扇区数少1
  8. dec ax ;已经读了一个扇区,扇区总数减1
  9. @1:
  10. cmp ax,0 ;考虑实际长度小于等于512个字节的情况
  11. jz direct

因为已经约定了用户程序的头部4个字节是用户程序的总长度,所以这里取总长度到[dx:ax]中,把[dx:ax]除以512,就能得到有几个扇区。dx存放余数,ax存放商。

如果dx==0,那么就把ax减一(因为前面已经读了一个扇区),继续执行@1;如果dx!=0,那么剩余的扇区数就是ax,然后跳到@1;

开始执行@1处的代码时,ax已经保存了还要读取的扇区数,但是这个值也有可能为0,如果为0,就不用再读取了, jz direct就可以;如果不为0,就执行下面的代码。

好了,如果你觉得上面说得不够清楚,那么看这个简单的流程图吧:

  1. ;读取剩余的扇区
  2. push ds ;以下要用到并改变DS寄存器
  3. mov cx,ax ;循环次数(剩余扇区数)
  4. @2:
  5. mov ax,ds
  6. add ax,0x20 ;得到下一个以512字节为边界的段地址
  7. mov ds,ax
  8. xor bx,bx ;每次读时,偏移地址始终为0x0000
  9. inc si ;下一个逻辑扇区
  10. call read_hard_disk_0
  11. loop @2 ;循环读,直到读完整个功能程序
  12. pop ds ;恢复数据段基址到用户程序头部段

mov ax,ds

add ax,0x20

mov ds,ax ;这三行表示调整ds的位置,让ds指向最后读入的块的末尾,也就是将要读入的块的开始。其他语句都好理解,这里就不解释了。

接下来是处理段的重定位表。我们要修正每个表项的值。

为什么要修正呢?看图就明白了。

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

我们看看代码:

  1. calc_segment_base: ;计算16位段地址
  2. ;输入:DX:AX=32位物理地址
  3. ;返回:AX=16位段基地址
  4. push dx
  5. add ax,[cs:phy_base]
  6. adc dx,[cs:phy_base+0x02]
  7. shr ax,4
  8. ror dx,4
  9. and dx,0xf000
  10. or ax,dx
  11. pop dx
  12. ret

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位不用管)。

为什么要求出段基地址呢?因为在用户程序中,对段寄存器赋值,都是从这里引用的。

  1. ;计算入口点代码段基址
  2. direct:
  3. mov dx,[0x08]
  4. mov ax,[0x06]
  5. call calc_segment_base
  6. mov [0x06],ax ;回填修正后的入口点代码段基址
  7. ;开始处理段重定位表
  8. mov cx,[0x0a] ;需要重定位的项目数量
  9. mov bx,0x0c ;重定位表首地址
  10. realloc:
  11. mov dx,[bx+0x02] ;32位地址的高16
  12. mov ax,[bx]
  13. call calc_segment_base
  14. mov [bx],ax ;回填段的基址
  15. add bx,4 ;下一个重定位项(每项占4个字节)
  16. loop realloc
  17. jmp far [0x04] ;转移到用户程序

只要参考本文开头的用户程序头部示意图,上面这段代码不难理解。

需要说明的是 jmp far [0x04] ;这个是16位间接绝对远转移指令。一定要使用关键字far。处理器执行这条指令的时候,会访问DS所指向的数据段,从偏移地址0x04处取出两个字(低字是偏移地址,高字是段基址),用低字代替IP的内容,用高字代替CS的内容,于是就可以转移了。

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

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

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

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

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

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

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

  4. ASM:《X86汇编语言-从实模式到保护模式》第8章:实模式下硬盘的访问,程序重定位和加载

        第八章是一个非常重要的章节,讲述的是实模式下对硬件的访问(这一节主要讲的是硬盘),还有用户程序重定位的问题.现在整理出来刚好能和保护模式下的用户程序定位作一个对比. ★PART1:用户程序的重 ...

  5. ASM:《X86汇编语言-从实模式到保护模式》1-4章:处理器,内存和硬盘基础

    其实很久之前就学完了实模式了,但是一直没有总结,感觉现在直接在书上做笔记的弊端就是有些知识点不能很很深刻地记下来(毕竟手写最明显的优点就是能深刻地记住知识,但是就是用太多的时间罢了).一下内容都是一些 ...

  6. ASM:《X86汇编语言-从实模式到保护模式》越计卷:实模式下对DMA和Sound Blaster声卡的控制

    说实话越计卷作者用了16页(我还是删过的),来讲怎么控制声卡,其实真正归纳起来就那么几点. ★PART1:直接存储访问 1. 总线控制设备(bus master) 在硬件技术不发达的早期,处理器是最重 ...

  7. 存储器的保护(二)——《x86汇编语言:从实模式到保护模式》读书笔记19

    接着上一篇博文说. 5.代码段执行时的保护 每个代码段都有自己的段界限.同栈段一个道理,有效界限和G位相关. G=0:有效界限 = 描述符中的段界限 G=1:有效界限 = 描述符中的段界限值 * 0x ...

  8. 8086实时时钟实验(二)——《x86汇编语言:从实模式到保护模式》读书笔记06

    上次我们说了代码,这次我们说说怎样看到实验结果. 首先编译源文件(我的源文件就在当前路径下,a盘和c盘在上一级目录下): nasm -f bin c08_mbr.asm -o c08_mbr.bin ...

  9. 数据段描述符和代码段描述符(二)——《x86汇编语言:从实模式到保护模式》读书笔记11

    这篇博文,我们编写一个C语言的小程序,来解析数据段或者代码段描述符的各个字段.这样我们阅读原书的代码就会方便一点,只要运行这个小程序,就可以明白程序中定义的数据段或者代码段的描述符了. 这段代码,我用 ...

随机推荐

  1. android studio中使用recyclerview小白篇(一)

    本人就是小白,昨天在使用listview时,看到说有更好的控件出来了,在V7包中,需要SDK21及以上,那就试着用用吧,今天试了一天,终于弄的能简单使用了,分享一下. 怎么导入这个recycleyvi ...

  2. android studio 程序员有福了—从layout自动生成viewholder类

    狂点这里下载 超级牛逼的插件啊,比那些使用SparseArray的强太多了! 在android studio 1.0上测试,没有问题. 不说了直接说功能 Android Toolbox Plugin ...

  3. 使用Privoxy转化SSH到HTTP代理

    为什么要进行转换? 一般我们很容易找到通过SOCKS5代理的方法,如SSH,但是很多浏览器或是软件只支持HTTP方式,所以就需要将我们的SSH代理模式转为HTTP代理方式  如何转换? 使用Privo ...

  4. java 获取前几天时间

    java   获取前几天时间 一.toLocaleString()函数过时: //当前时间   Date endDate=new Date();   String endTime=endDate.to ...

  5. c# 字符串填充占位

    C#  字符串PadLeft函数的使用 1.Demo: 需求: 将111改变成0000111 使用字符串PadLeft函数可以解决: int num = 111; string s= num.ToSt ...

  6. Android Android环境安装

    Android环境安装 一.下载: 所需:eclipse.sdk.jdk.adt 参考 工具下载地址:http://www.androiddevtools.cn/ 二安装: 1.eclipse: 选择 ...

  7. dubbo异步调用原理 (1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 一.使用方式 服务提供方不变,调用方代码如下: 1     <dubbo:reference id=& ...

  8. java基础之语法和开发规则

    一. 代码书写的规则 以下面为例: 先写好结构 注意:为了避免错误,写代码时先把括号打齐,然后再补内容,每个”{}”里的内容开始写时要相比上一行多8个空格.为了方便可以用键盘上的 键代替(一般情况下时 ...

  9. js window.open()打开的页面关闭后刷新父页面

    function test(){ var winObj = window.open(URL); var loop = setInterval(function(){ if(winObj.closed) ...

  10. ConcurrentHashMap原理详解

    参考链接:https://www.cnblogs.com/chengxiao/p/6842045.html https://www.cnblogs.com/ITtangtang/p/3948786.h ...