电脑技术学习

FreeBSD启动扇区代码分析

dn001

#
# Copyright (c) 1998 Robert NordIEr
# All rights reserved.
#
# Redistribution and use in source and binary forms are freely
# permitted provided that the above copyright notice and this
# paragraph and the following disclaimer are duplicated in all
# such forms.
#
# This software is provided "AS IS" and without any express or
# implied warranties, including, without limitation, the implied
# warranties of merchantability and fitness for a particular
# purpose.
#
以上的Coyright就不用翻译了。 # $FreeBSD: src/sys/boot/i386/boot0/boot0.s,v 1.27 2003/11/20 20:28:18 jhb Exp $ 以上供版本管理软件使用  # A 512-byte boot manager.
    .set NHRDRV,0x475   # Number of hard drives
    .set ORIGIN,0x600   # Execution address
    .set FAKE,0x800    # Partition entry
    .set LOAD,0x7c00   # Load address
    .set PRT_OFF,0x1be   # Partition table
    .set TBL0SZ,0x3   # Table 0 size
    .set TBL1SZ,0xb   # Table 1 size
    .set MAGIC,0xaa55   # Magic: bootable
    .set B0MAGIC,0xbb66   # Identification
    .set KEY_ENTER,0x1c   # Enter key scan code
    .set KEY_F1,0x3b     # F1 key scan code
    .set KEY_1,0x02     # #1 key scan code
#
# Addresses in the sector of embedded data values.
# Accessed with negative offsets from the end of the relocated sector (%ebp).
#
    .set _NXTDRV,-0x48     # Next drive
    .set _OPT,-0x47       # Default option
    .set _SETDRV,-0x46     # Drive to force
    .set _FLAGS,-0x45     # Flags
    .set _TICKS,-0x44     # Timeout ticks
    .set _FAKE,0x0       # Fake partition entry
    .set _MNUOPT,0xc # Menu options
  以上是定义相关的参数值,例如“.set NHRDRV,0x475类似于C语言中的“#define NHRDRV 0x475    .globl start   # Entry point
    .code16     # This runs in real mode
#
# Initialise segments and registers to known values.
# segments start at 0.
# The stack is immediately below the address we were loaded to.
#
start:
    cld       # String ops inc
    xorw %ax,%ax   # Zero
    movw %ax,%es   # Address
    movw %ax,%ds   # data
    movw %ax,%ss   # Set up
    movw $LOAD,%sp   # stack
  以上代码: 1)首先使用“cld指令清除方向标志,使得以下的进行“rep操作时SI和DI的值递增。 2)使ax清零,并使除代码段cs外的另外两个数据段寄存器es、ds和堆栈段ss清零。当然,此时cs由于reset或初始上电已经为零了。 3)BIOS已经把引导扇区的512字节的内容读入到了0:0x7c00处,movw $LOAD,%sp 使得堆栈指针指向扇区代码(或曰本段代码 0:0x7c00)的顶部。虽然堆栈向下生长可能会影响代码的内容,但下面我们马上就把位于0:7c00处代码移到其他地方去执行。  #
# Copy this code to the address it was linked for
#
    movw %sp,%si     # Source
    movw $start,%di   # Destination
    movw $0x100,%cx   # Word count
    rep         # Relocate
    movsw         # code
把位于0:7c00处的代码搬移到0:0x600处。注意,此时由于代码连接的重定向,$start=0x600。#
# Set address for variable space beyond code, and clear it.
# Notice that this is also used to point to the values embedded in the block,
# by using negative offsets.
    movw %di,%bp   # Address variables
    movb $0x8,%cl   # Words to clear
    rep       # Zero
    stosw       # them
通过以上一段代码的执行,本代码已被搬移到0:0x600处,此时si=di=0x600+0x100,以上代码把di的值保存到bp,bp此时指向本程序搬移后的未用的空间的首部,且把此bp所指的16字节空间清零。以上过程如下图所示:             ┏>0:0x600 ┏━━━━━┓             ┃     ┃     ┃             ┃     ┃  搬  ┃             ┃     ┃  移  ┃             ┃     ┃  之  ┃             ┃     ┃  后  ┃             ┃     ┃  的  ┃             ┃     ┃  代  ┃             ┃     ┃  码  ┃             ┃     ┃     ┃             ┃ 0:0x7ff ┣━━━━━┫             ┃     ┃  0   ┃<-bp指向这里(0:0x800),以此开始的16字节被清零。             ┃     ┣━━━━━┫以下所称的fake partition entry就是指这里。             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┣━━━━━┫             ┃     ┃  0   ┃             ┃     ┗━━━━━┛ 0:0x7c00 ┏━━━━━┓ ┛      ┃     ┃      ┃  搬  ┃      ┃  移  ┃      ┃  之  ┃      ┃  前  ┃      ┃  的  ┃      ┃  代  ┃      ┃  码  ┃      ┃     ┃ 0:0x7dff ┗━━━━━┛            图(二)#
# Relocate to the new copy of the code.
#
    incb -0xe(%di) # Sector number
    jmp main-LOAD+ORIGIN # To relocated code
把以上清零的16字节的第二个字节置为1,表示我们已经读取了一个分区。然后跳转到搬移之后的新代码的main处执行。#
# Check what flags were loaded with us, specifically, Use a predefined Drive.
# If what the bios gives us is bad, use the '0' in the block instead, as well.
#
main:
    testb $0x20,_FLAGS(%bp)    # Set number drive?
    jnz main.1           # Yes
    testb %dl,%dl         # Drive number valid?
    js main.2           # Possibly (0x80 set)
main.1:
    movb _SETDRV(%bp),%dl # Drive number to use
上面说过,BIOS把磁盘的引导扇区读入到内存之后,其dl的内容表示启动设备,但我们安装好FreeBSD之后,我们可以改变此引导扇区的内容,其中的一个改变就是可以使我们可以“手动指定我们实际安装FreeBSD的分区,如果我们希望指定FreeBSD所在的boot分区,那么我们在bp-0x45处的位置(即_FLAGS(%bp)处)的bit 0x20置1,那么上面的“movb _SETDRV(%bp),%dl一句中movb_SETDRV(%bp),%dl(即bp-0x46)即指向我们“手动指定FreeBSD所在分区代码,例如,IDE的C、D盘(严格来说是第一个物理磁盘的第一个和第二个分区)的代码分别为 0x80和0x81。如果没有“手动指定启动分区,那么,我们缺省使用机器当前启动的分区,上面说过,机器当前启动的分区代码放在dl中。因为FreeBSD Boot Manager 不可能安装到软盘(如果从软盘启动则dl为0),所以,使用testb %dl,%dl来判断驱动器代码是否合法(volid)。 有关_FLAGS(%bp)中其他bit位表示的意义,在随后的代码分析中慢慢给你道来。#
# Whatever we decided to use, now store it into the fake
# partition entry that lives in the data space above us.
#
main.2:
    movb %dl,_FAKE(%bp) # Save drive number
    callw putn # To new line
    pushw %dx # Save drive number
以上第一句把FreeBSD启动分区的代码保存到_FAKE(%bp)(bp-0)处,也就是说,上图(二)的bp处保存的是FreeBSD启动分区的代码(_FAKE=0)。 “callw putn一句在屏幕上打印“回车和“换行,“pushw %dx一句把启动分区的值压入堆栈。#
# Start out with a pointer to the 4th byte of the first table entry
# so that after 4 iterations it's beyond the end of the sector.
# and beyond a 256 byte boundary and has overflowed 8 bits (see next comment).
# (remember that the table starts 2 bytes earlier than you would expect
# as the bootable flag is after it in the block)
#
    movw $(partbl+0x4),%bx # Partition table (+4)
    xorw %dx,%dx # Item number
以上代码首先把%bx指向分区表partbl的的第四个字节,这里存放的是分区类型,如82表示Linux Native分区83表示Linux Swap 分区,有关分区表的细节请详见本文的尾部。然后dx清零,此后,dx将作为遍历磁盘分区的列举代号使用。启动分区代码dl的原来的值在上面已经压入了堆栈保存。#
# Loop around on the partition table, printing values until we
# pass a 256 byte boundary. The end of loop test is at main.5.
#
main.3:
    movb %ch,-0x4(%bx)   # Zero active flag (ch == 0)
    btw %dx,_FLAGS(%bp)   # Entry enabled?
    jnc main.5       # No
上面首先使得第一个分区的活动标志为0,标志它不为活动标志,因为ch的值为0。至于第二句“btw %dx,_FLAGS(%bp)中的_FLAGS(%bp)是上面我们说到的“手动指定我们实际安装FreeBSD的分区代码。其中的bit 0x20我们在上面已经提到过。_FLAGS(%bp)中的其他位表示是否我们需要检查相应的磁盘分区。缺省情况下,我们需要检查所有的磁盘分区。检查磁盘分区看是否有可以启动的磁盘分区,例如,可能磁盘上的某个分区为WindowsXP或者是Linux等。如果我们没有改变在磁盘上该处的值,则相应的bit位的值为0,表示所有的分区都要检查(因为此时_FLAGS(%bp)中的值为0),否则,只针对FLAGS(%bp)中相应的bit位未被设置为1的分区进行检查。 大家知道,FreeBSD Manager启动时可能出现以下的提示:F1  FreeBSD
F2  ??
F3  BSD
F4  ??
Default  F1
其中,上面的提示中出现了令人讨厌的“??,为了避免出现“??的提示,我们可以设置相应的第一分区和第四分区不检查,就需要正确设置_FLAGS(%bp)中的bit位。设置好后,屏幕可能出现以下的提示:F1  FreeBSD
F2  BSD
Default  F1
#
# If any of the entries in the table are
# the same as the 'type' in the slice table entry,
# then this is an empty or non bootable partition. Skip it.
#
    movb (%bx),%al     # Load type
    movw $tables,%di   # Lookup tables
    movb $TBL0SZ,%cl   # Number of entries
    repne         # Exclude
    scasb         # partition?
    je main.5       # Yes
我们从上面已经知道起始(%bx)指向的是MBR中分区信息1(16字节)的位置(见图(三)),以上代码在“忽略的分区类型$tables中查找看是否本分区是不可启动的或者不合法的分区。不可启动的或者不合法的分区类型有3($TBL0SZ=3)个,它们是“0x0, 0x5, 0xf,见下面的$tables处。如果是不可启动的或者不合法的分区类型则跳转到main.5,进行下一轮循环。#
# Now scan the table of known types
#
    movb $TBL1SZ,%cl     # Number of entries
    repne           # Known
    scasb           # type?
    jne main.4         # No
#
# If it matches get the matching element in the
# next array. if it doesn't, we are already
# pointing at its first element which points to a "?".
#
    addw $TBL1SZ,%di   # Adjust
main.4:
    movb (%di),%cl # Partition
    addw %cx,%di # description
    callw putx # Display it
上面检查看所检查的分区类型是否为我们知道的分区类型,知道的分区类型有11($TBL1SZ=0xb)个,它们是:“0x1, 0x4, 0x6, 0xb, 0xc, 0xe, 0x83,0x9f, 0xa5, 0xa6, 0xa9,见下面的$tables处。如果不是以上的类型,则跳转到main.4。那么,(%di)所指的字串是“??,如果分区类型是“0x1, 0x4, 0x6, 0xb, 0xc, 0xe, 0x83,0x9f, 0xa5, 0xa6, 0xa9之一,则(%di)所指的字串是“Dos、“Linux、“FreeBSD或“BSD等。见下面的“os_misc、“os_dos、“os_linux、“os_freebsd、“os_bsd等标记。 callw putx调用putx函数,在屏幕上打印:“Fx  XXX。其中XXX为DOS、“Linux、“FreeBSD或“BSD等。main.5:
    incw %dx # Next item
    addb $0x10,%bl # Next entry
    jnc main.3 # Till done
遍历磁盘分区的举代号dx加1,重复下一轮循环查找。bl加上0x10(0x10=16)表示寻址到下一个分区信息(加16字节)入口。循环直到255字节边界。#
# Passed a 256 byte boundary..
# table is finished.
# Add one to the drive number and check it is valid,
#
    popw %ax         # Drive number
    subb $0x80-0x1,%al   # Does next
    cmpb NHRDRV,%al     # drive exist? (from BIOS?)
    jb main.6         # Yes
“popw %ax把上面压入堆栈的bx(当前的启动扇区)值弹出到ax中。例如,如果计算机是从软盘启动的则dl=0,若是从IDE的C、D盘启动的则dl分别为 0x80和0x81。然而,FreeBSD的Boot Manerger不能够安装到软盘上,所以,dl只能为0x80、0x81,0x82...等。在计算机的BIOS地址0:0x475处存放的是计算机的硬盘的数目,“subb $0x80-0x1,%al一句等于“sub$0x79,%al,例如,即当前驱动器如果是C盘,则al的值是ox80-0x79=1,然后再与计算机的硬盘的数目比较,如果当前所在硬盘不是最后一个硬盘,则直接跳转到main.6。如果当前所在硬盘是最后一个硬盘,则继续执行。# If not then if there is only one drive,
# Don't display drive as an option.
#
    decw %ax     # Already drive 0?
    jz main.7     # Yes
如果只有一个硬盘,则直接跳转到main.7,这样,本计算机只有一个硬盘,所以不用显示其他 磁盘相关的提示。# If it was illegal or we cycled through them,
# then go back to drive 0.
#
    xorb %al,%al # Drive 0
下面的内容表示多于一个磁盘的情况。此时“al清0,与磁盘列举相关。#
# Whatever drive we selected, make it an ascii digit and save it back
# to the "next drive" location in the loaded block in case we
# want to save it for next time.
# This also is part of the printed drive string so add 0x80 to indicate
# end of string.
#
main.6:
    addb $'0'|0x80,%al     # Save next
    movb %al,_NXTDRV(%bp)     # drive number
    movw $drive,%di       # Display
    callw putx       # item
首先,在_NXTDR(%bp)处置入“0字符高位置1的字符,以代表第二个驱动器,然后在屏幕上显示“Fx Drive,表示更换另外的磁盘启动。注意,在调用putx之前,di中保存的是下面字串“Drive 的首地址。dl中存放的是当前遍历的到的可启动的或者合法的分区类型递增序数,al与dl是不同的,al是ASCII码,dl是“Fx中的x值。#
# Now that we've printed the drive (if we needed to), display a prompt.
# Get ready for the input byt noting the time.
#
main.7:
    movw $prompt,%si   # Display
    callw putstr     # prompt
    movb _OPT(%bp),%dl   # Display
    decw %si       # default
    callw putkey     # key
    xorb %ah,%ah     # BIOS: Get
    int $0x1a       # system time
    movw %dx,%di       # Ticks when
    addw _TICKS(%bp),%di  # timeout
上面的代码首先在屏幕上打印出字符串“Default: ,缺省启动的磁盘号放在“_OPT(%bp)中,这里有个小小的技巧,在执行“decw %si和“callw putkey两句后屏幕会显示“Fx,x是_OPT(%bp)的ASCII。然后取得当前的tickes放到%di中,等待用户按键超时的时间从_TICKS(%bp)中取出,加到当前的tickes即是最后超时时间到的tickes。#
# Busy loop, looking for keystrokes but
# keeping one eye on the time.
#
main.8:
    movb $0x1,%ah   # BIOS: Check
    int $0x16     # for keypress
    jnz main.11     # Have one
    xorb %ah,%ah     # BIOS: Get
    int $0x1a     # system time
    cmpw %di,%dx     # Timeout?
    jb main.8     # No
等待用户按下“Fx键,同时检查当前等待是否超时,如果有用户按键则跳转到main.11, 如果超时时间不到则继续等待。#
# If timed out or defaulting, come here.
#
main.9:
    movb _OPT(%bp),%al # Load default
    jmp main.12 # Join common code
超时时间到,此时表示用户使用缺省分区启动,把缺省的启动分区号置入al中,然后跳转 到main.12。#
# User's last try was bad, beep in displeasure.
# Since nothing was printed, just continue on as if the user
# hadn't done anything. This gives the effect of the user getting a beep
# for all bad keystrokes but no action until either the timeout
# occurs or the user hits a good key.
#
main.10:
    movb $0x7,%al # Signal
    callw putchr # error
用户输入错误,只是响铃提示,其他什么也不发生。#
# Get the keystroke.
#
main.11:
    xorb %ah,%ah     # BIOS: Get
    int $0x16       # keypress
    movb %ah,%al     # Scan code
用户按下了一个键,把键值扫描码放到al中。#
# If it's CR act as if timed out.
#
    cmpb $KEY_ENTER,%al     # Enter pressed?
    je main.9         # Yes
如果用户按下“Enter键,和超时等同处理,这样,就启动缺省的boot分区。#
# Otherwise check if legal
# If not ask again.
#
    subb $KEY_F1,%al     # Less F1 scan code
    cmpb $0x4,%al       # F1..F5?
    jna main.12         # Yes
    subb $(KEY_1 - KEY_F1),%al     # Less #1 scan code
    cmpb $0x4,%al         # #1..#5?
    ja main.10           # No
如果是除“Enter键外其他的键,则检查是不是F1...F5键,如果不是,表示输入不合法,跳回到main.10处理。#
# We have a selection.
# but if it's a bad selection go back to complain.
# The bits in MNUOPT were set when the options were printed.
# Anything not printed is not an option.
#
main.12:
    cbtw           # Option
    btw %ax,_MNUOPT(%bp)  # enabled?
    jnc main.10       # No
如果是F1...F5键,则检查是否在我们提示的范围内,其中,_MNUOPT(%bp)的相应bit位为1,表示是一个合法的选项,如果不是,跳回到 main.10处理。#
# Save the info in the original tables
# for rewriting to the disk.
#
    movb %al,_OPT(%bp)     # Save option
把我们按下的F1...F5键保存到_OPT(%bp)位置。    movw $FAKE,%si       # Partition for write
    movb (%si),%dl       # Drive number
把原来的启动分区代码取回到dl中。    movw %si,%bx         # Partition for read
    cmpb $0x4,%al         # F5 pressed?
    pushf           # Save
    je main.13       # Yes
如果我们按下的是F5键则直接跳转到main.13处理。    shlb $0x4,%al       # Point to
    addw $partbl,%ax       # selected
    xchgw %bx,%ax        # partition
    movb $0x80,(%bx)       # Flag active
上面,我们从按键Fx选择中得到图(三)中的我们选择的四个分区信息中的某一分区信息,上面计算出的bx为我们选择的分区信息的首地址,我们把此选择到的分区信息的第一个个字节置为0x80表示它是当前的活动分区。#
# If not asked to do a write-back (flags 0x40) don't do one.
#
main.13:
    pushw %bx           # Save
    testb $0x40,_FLAGS(%bp)     # No updates?
    jnz main.14         # Yes
    movw $start,%bx     # Data to write
    movb $0x3,%ah       # Write sector
    callw intx13       # to disk
检查回写标志_FLAGS(%bp)的bit位0x40为,如果设置的是可回写,则把当前选择到的boot分区作为下次缺省的启动分区。main.14:
    popw %si       # Restore
    popf         # Restore
#
# If going to next drive, replace drive with selected one.
# Remember to un-ascii it. Hey 0x80 is already set, cool!
#
    jne main.15         # If not F5
恢复上面保存的si和标志寄存器的内容。如果不是按键F5,则直接跳转到main.15去执行。    movb _NXTDRV(%bp),%dl     # Next drive
    subb $'0',%dl         # number
否则的话,我们选择下一个驱动器作为启动盘。#
# load selected bootsector to the LOAD location in RAM.
# If it fails to read or isn't marked bootable, treat it
# as a bad selection.
# XXX what does %si carry?
#
main.15:
    movw $LOAD,%bx     # Address for read
    movb $0x2,%ah       # Read sector
    callw intx13     # from disk
    jc main.10       # If error
把我们上面选择到的分区读到0x7c00处,就象我们刚刚才加电启动一样,只是活动分区改变了而已。如果发生读错误则直接跳转到main.10。使用户重新选择启动分区。     cmpw $MAGIC,0x1fe(%bx)     # Bootable?     jne main.10       # No 判断引导扇区的最后两个字节是否是“0x55AA,如果不是,则表示此扇区是不可引导的,或不合法的引导扇区则直接跳转到main.10。使用户重新选择启动分区。     pushw %si       # Save     movw $crlf,%si     # Leave some     callw puts       # space     popw %si       # Restore 打印“回车和“换行。     jmp *%bx       # Invoke bootstrap 跳转到我们选择的引导扇区去执行。整个Boot Manager代码到此执行完毕。#
# Display routines
#
putkey:
    movb $'F',%al     # Display
    callw putchr     # 'F'
    movb $'1',%al     # Prepare
    addb %dl,%al     # digit
    jmp putstr.1     # Display the rest
“putkey函数在屏幕上打印“F1、“F2或“F3等。如果dl为0则打印“F1, 如果dl为1则打印“F2,如果dl为3则打印“F3。和调用“putstr在屏幕上打印 es:si指向的以最高位置1为结束字符的字符串。#
# Display the option and note that it is a valid option.
# That last point is a bit tricky..
#
putx: #首先,把_MNUOPT(%bp)的第dx位(bit)置1,表示此菜单选项被显示。然后在屏幕上打印空格和es:di指向的以最高位置1为结束字符的字符串。    btsw %dx,_MNUOPT(%bp)     # Enable menu option
    movw $item,%si         # Display
    callw putkey         # key
    movw %di,%si         # Display the rest
puts: #调用“putstr在屏幕上打印es:si指向的以最高位置1为结束字符的字符串。     callw putstr         # Display string putn: #“putn打印“回车/换行后在屏幕上打印es:si指向的以最高位置1为结束字符的字符串。    movw $crlf,%si         # To next line
putstr: #“putstr在屏幕上打印es:si指向的以最高位置1为结束字符的字符串。
    lodsb           # Get byte
    testb $0x80,%al     # End of string?
    jnz putstr.2       # Yes
putstr.1:
    callw putchr       # Display char
    jmp putstr         # Continue
putstr.2:
    andb $~0x80,%al       # Clear MSB
putchr:
    pushw %bx         # Save
    movw $0x7,%bx      # Page:attribute
    movb $0xe,%ah       # BIOS: Display
    int $0x10         # character
    popw %bx         # Restore
    retw           # To caller
“putchr在屏幕上显示“al中的字符。# One-sector disk I/O routine
intx13:
    movb 0x1(%si),%dh       # Load head
    movw 0x2(%si),%cx       # Load cylinder:sector
    movb $0x1,%al         # Sector count
    pushw %si           # Save
    movw %sp,%di         # Save
    testb $0x80,_FLAGS(%bp)     # Use packet interface?
    jz intx13.1         # No
    pushl $0x0         # Set the
    pushl 0x8(%si)       # LBA address
    pushw %es         # Set the transfer
    pushw %bx         # buffer address
    push $0x1         # Block count
    push $0x10         # Packet size
    movw %sp,%si       # Packet pointer
    decw %ax         # Verify off
    orb $0x40,%ah       # Use disk packet
intx13.1:
    int $0x13         # BIOS: Disk I/O
    movw %di,%sp       # Restore
    popw %si         # Restore
    retw           # To caller
# Menu strings
item:
    .ascii " ";   .byte ' '|0x80
prompt:
    .ascii "nDefault:".byte ' '|0x80
crlf:
    .ascii "r";   .byte 'n'|0x80
# Partition type tables
tables:
#
# These entries identify invalid or NON BOOT types and partitions.
#
    .byte 0x0, 0x5, 0xf
#
# These values indicate bootable types we know the names of
#
    .byte 0x1, 0x4, 0x6, 0xb, 0xc, 0xe, 0x83
    .byte 0x9f, 0xa5, 0xa6, 0xa9
#
# These are offsets that match the known names above and point to the strings
# that will be printed.
#
    .byte os_misc-.     # Unknown
    .byte os_dos-.     # DOS
    .byte os_dos-.     # DOS
    .byte os_dos-.     # DOS
    .byte os_dos-.     # Windows
    .byte os_dos-.     # Windows
    .byte os_dos-.     # Windows
    .byte os_linux-.     # Linux
    .byte os_bsd-.     # BSD/OS
    .byte os_freebsd-.     # FreeBSD
    .byte o

标签: