操作系统:Linux 0.00 汇编语法要点解释

这篇文章的分析对象是赵炯的《Linux 内核完全注释》第 4.9 节的“一个简单的多任务内核实例”。

这里将对 boot.s 和 head.s 两个程序的关键语句进行汇编语法层面的分析。

语法格式的不同

汇编语言的语法主要分为两种:Intel 汇编语法、AT&T 汇编语法。

as86、MASM、TASM、NASM 等汇编器采用的是 Intel 汇编语法,而 GNU as (gas) 采用的是 AT&T 汇编语法。但即使采用同一汇编语法的汇编器,在具体的语法上也可能存在差别,需要特别注意。

关于两种语法的差异,可以参考文章《Linux 汇编语言开发指南》

我们这里要分析的引导程序 boot.s 用的是 Intel 语法,内核程序 head.s 用的是 AT&T 语法。具体细节可以查阅《Linux 内核完全注释》的第 3 章。

boot.s

Intel 汇编语法中,注释用分号开头。

  • entry start:指明程序的入口在 start 标签处
    jmpi go, #BOOTSEG ; 井号是可选的立即数常量前缀符
  • int 0x13:int 指令用于产生软件中断,操作数即为中断号
  • rep movw:重复执行 movw 操作,操作次数由当前 ECX 寄存器的值确定,源操作数默认为 DS:SI,目的操作数为 ES:DI
  • lidt idt_48:这里的标签 idt_48 就相当与内存地址,即从该标签所在位置加载 6 个字节内容到 IDTR 中
  • lmsw ax:加载机器状态字,即将 AX 的内容加载到控制寄存器中
  • .org 510:从当前位置开始,填充 0 直到 510 字节处,指令语法为 .org new_loc, fill,其中 fill 操作数表示要填充的内容,可省略,即默认为 0

head.s

  • .text:表明只读代码段的开始
  • startup_32::程序入口标签
  • movl $0x10, %eax$ 是立即数的前缀,% 是寄存器的前缀,当操作数的大小可能产生歧义时,由指令的最后一个字母来确定操作数大小,movl 中的 l 表示 long,即操作数为 4 字节
  • lss init_stack, %esp:lss 是加载堆栈段寄存器(SS)的指令,init_stack 标签对应的内存地址加载到 ESP 寄存器中
  • lea idt(,%ecx,8), %esi:间接内存引用,完整指令格式为 lea section:disp(base, index, scale), %esi,其中 section 是所在段,disp 是偏移量,scale 是比例因子,等价于 Intel 语法的 lea esi, section:[base + index*scale],即 lea esi, [idt + ecx*8],表示把 idt 偏移 ecx*8 处的地址存入 esi 寄存器中。
  • movl %eax, (%esi):括号表示寄存器间接寻址
  • mov %edx, 4(%esi):等价于 Intel 格式的 [%esi + 4]
  • pushfl:将标志寄存器压栈
  • popfl:将标志寄存器弹栈
  • iret:中断返回指令
  • shl $1, %ebx:ebx 寄存器逻辑左移 1 位,shr 表示逻辑右移,sal 和 sar 对应算术左移和右移
  • jb 1f:跳转到标签 1 处。汇编中数字 0~9 为局部标签,不同地方可重复定义该标签。此处 f 表示 forward,即往前跳转到未执行的语句,对应还有 b,表示 backward,即往回跳转到执行过的语句。因此这里的 jb 1f 表示往前跳转到还未执行过的标签 1。
  • lret:远返回指令,从一个段返回到另一个段,Intel 语法为 ret far
  • ljmp $section, $offset:远转移,从一个段跳转到另一个段,Intel 语法为 jmp far ip, cs,其中 jmp far 可简写为 jmpi
  • lcall $section, $offset:远调用,调用其它段的程序,Intel 语法为 call far ip, cscall far [cs:ip],后一种写法是提供一个内存地址 [cs:ip],该指令会读取从该内存地址开始的 6 字节操作数,高两字节对应 cs,低四字节对应 ip。远转移指令也可以采用这种 6 字节操作数的写法。

标签/标号

由于之前发现还有很多人没有正确理解汇编中的标签,因此在这里稍做解释。

汇编器编译汇编程序时,会有一个计数器,初始为零,用于计算当前指令相对于程序起始位置的偏移量。

标签的值,就是当汇编器编译到标签位置时,计数器的值。也就是说,标签的值就是其所在位置与程序起始位置的地址偏移量。

以 boot.s 程序为例,当它被加载到 0x7c00 的位置时,go 标签对应的值为 0x05,此时处于实模式下,因此 jmp bootseg:go 等价于 jmp 0x07c0:0x05,即 jmp 0x7c05。

下面是 NASM 汇编器版本的 boot.s 程序,左边一列表示计数器的值,即程序内部的地址偏移,可对照该代码理解标签的含义。

          bootseg equ 0x07c0
          sysseg equ 0x1000
          syslen equ 17
          start:
00000000      jmp bootseg:go
          go:
00000005      mov ax,cs
00000007      mov ds,ax
00000009      mov ss,ax
0000000B      mov sp,0x0400

          load_syetem:
0000000E      mov dx,0x0000
00000011      mov cx,0x0002
00000014      mov ax,sysseg
00000017      mov es,ax
00000019      xor bx,bx
0000001B      mov ax,0x200+syslen
0000001E      int 0x13
00000020      jnc ok_load
          die:
00000022      jmp die

          ok_load:
00000024      cli
00000025      mov ax,sysseg
00000028      mov ds,ax
0000002A      xor ax,ax
0000002C      mov es,ax
0000002E      mov cx,0x2000
00000031      sub si,si
00000033      sub di,di
00000035      rep movsb

00000037      mov ax,bootseg
0000003A      mov ds,ax
0000003C      lidt [idt_48]
00000041      lgdt [gdt_48]
00000046      mov ax,0x0001
00000049      lmsw ax
0000004C      jmp 8:0

          gdt:
00000051      dw 0,0,0,0
00000059      dw 0x07ff
0000005B      dw 0x0000
0000005D      dw 0x9a00
0000005F      dw 0x00c0

00000061      dw 0x07ff
00000063      dw 0x0000
00000065      dw 0x9200
00000067      dw 0x00c0

          idt_48:
00000069      dw 0,0,0
          gdt_48:
0000006F      dw 0x7ff
00000071      dw 0x7c00+gdt,0
00000075      times 510-($-$$) db 0
000001FE      dw 0xaa55

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注