[Intel汇编-NASM]程序的加载以及硬盘访问

来源:互联网 发布:mysql修改数据库命令 编辑:程序博客网 时间:2024/05/03 22:59

1. 用户程序的结构:

    1) 一般源程序都以段的形式进行组织,这样可以使逻辑更加清晰,在NASM中使用section关键字定义一个段,形式是:section 段名

    2) 程序可以用段名来引用段,但是NASM编译器并不关心段的具体用途,或者说是根本不知道段的用途(代码段还是数据段等),同时NASM对段的数量也没有任何限制,如果代码中没有定一段则整个程序自成一段;

    3) 定义段的同时可以定义段的一些属性,比如可以使用关键字align来定义段的对其方式,比如:section code align=16,这样就表示该段的其实地址是以16字节对齐的,即段的起始位置必须是16的整数倍;

!注意:该属性只影响段的起始位置的对其但不影响段的末尾对齐方式,事实上NASM也无法判断一个段的末尾,只有当遇到一个新的段的定义的时候才能知道前一个段结束了;

    4) 段的起始位置:就是该段中第一行指令的地址(指令可以是普通指令也当然可以是数据定义指令db、dw等等;

    5) 和加载程序之间的约定——应用程序头部Header:

        i. 在有操作系统的环境下编译完一个程序之后编译器会隐式地、默认地添加一个应用程序头部(位置处于程序的起始位置处);

        ii. 头部包含着加载器该如何加载该程序的一些信息,或者说是加载器和程序之间的某些约定或规范,而加载器通过这些信息将程序正确地加载进内存中;

        iii. 在有操作系统的环境下,应用程序头部和加载器都是操作系统负责的,但是在这里我们模拟一下这个操作系统的工作,即手写完成加载器和程序头部来模拟操作系统的这两个功能;


2. 用户程序和加载器的简单实现:

用户程序:默认程序已经正确加载到了内存的空闲位置,并且从定义的标号start处该是执行程序,作用是将两个数据段中的字符串打印到屏幕上,并且处理回车和换行两个控制符

!注意用户程序头部的定义,里面包含了程序大小、程序开始执行的入口、程序中各个段的起始位置等信息;

!其中重定位表的作用就是:编译后各项保存的是各段在源程序中的绝对汇编地址,经加载程序加载后就将各项修改成在物理内存中实际的地址,因此称为重定位表;

app.nas,编译后生成app.bin

; 应用程序头; 用于提供加载器相关加载信息; 是应用程序规范的一部分section header vstart=0app_sizeddapp_end; [APP_SIZE:0x00] 程序的大小(字节)app_entrydwstart; [APP_ENTRY:0x04] 入口处偏移地址app_entry_segddsection.code1.start; [APP_ENTRY_SEG:0x06] 入口处段地址; section.段名.start是NASM提供的伪指令,用于段起始位置在源程序中的绝对汇编地址; 绝对汇编地址是指相对于整个源程序头的偏移量,而整个程序头的绝对汇编地址是0; 绝对汇编地址是一个32位无符号数,因此使用dd表示c_realloc_tbldw(tbl_end - tbl_start) / 4; [C_REALLOC_TBL:0x0A] 重定位表表项数目tbl_start:; [TBL_START:0x0C]seg_addr_code1ddsection.code1.startseg_addr_code2ddsection.code2.startseg_addr_data1ddsection.data1.startseg_addr_data2ddsection.data2.startseg_addr_stackddsection.stack.starttbl_end:; section header end;;;;section stack align=16 vstart=0resb 256stack_end:; section stack end;;;;section data1 align=16 vstart=0msg0 db '  This is NASM - the famous Netwide Assembler. ' db 'Back at SourceForge and in intensive development! ' db 'Get the current versions from http://www.nasm.us/.' db 0x0d,0x0a,0x0d,0x0a db '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a db '     xor dx,dx',0x0d,0x0a db '     xor ax,ax',0x0d,0x0a db '     xor cx,cx',0x0d,0x0a db '  @@:',0x0d,0x0a db '     inc cx',0x0d,0x0a db '     add ax,cx',0x0d,0x0a db '     adc dx,0',0x0d,0x0a db '     inc cx',0x0d,0x0a db '     cmp cx,1000',0x0d,0x0a db '     jle @@',0x0d,0x0a db '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a db 0; section data1 end;;;;section data2 align=16 vstart=0msg1 db '  Welcome and enjoy NASM! ' db '2015-01-05' db 0; section data2 end;;;;section code1 align=16 vstart=0start:movax, [seg_addr_stack]movss, axmovsp, stack_endmovax, [seg_addr_data1]movds, axmovbx, msg0callput_string; 显示第一段信息; 在加载程序中将es指向header了pushword [es:seg_addr_code2]; 先将code2的偏移地址和段地址入栈movax, _start.beginpushaxretf; 利用retf修改cs:ip使其跳转至code2.continue:movax, [es:seg_addr_data2]movds, axmovbx, msg1callput_string; 使ds:bx指向msg1并输出jmp$; end start; 字符串控制宏以及显卡光标端口宏CHAR_TRAILequ0x00; 字符串结束符CHAR_RETequ0x0D; 回车符CHAR_NLequ0x0A; 换行符DCHAR_NONEequ0x0720; 显存中显示空的字PORT_CHOOSEequ0x3D4; 索引端口,用于选择子端口(8位) SUBPORT_HIGHequ0x0E; 子端口号SUBPORT_LOWequ0x0F; 这两个子端口分别存放光标位置的高位和低位PORT_DATAequ0x3D5; 数据端口,存放选定的端口中的数据(8位)VIDEO_SEG_BEGINequ0xB800; 显卡区域起始段地址; func put_string; <- [ds:bx]:msg0; colision register: es; 将msg0打印至屏幕put_string:pushes; 获取当前光标位置保存在ax中movdx, PORT_CHOOSEmoval, SUBPORT_HIGHoutdx, al; 选择一个子端口movdx, PORT_DATAinal, dxmovah, al; 从子端口中读取光标高位保存在ah中movdx, PORT_CHOOSEmoval, SUBPORT_LOWoutdx, almovdx, PORT_DATAinal, dx; 同理从子端口中读取光标低位保存在al中; 最终将整个结果保存在ax中; 目前ax存放着光标的位置.lp:movcl, [bx]; 读取一个字符保存在cl中cmpcl, CHAR_TRAIL; 判断该字符是否是结束符je.retcallput_char; 不是结束符就打印该字符incbx; 继续读取下一个字符jmp.lp.ret:popesret; func put_char; <- cl:当前读取的一个字符; colision register: ds, bxput_char:pushdspushbx; 备份; ds和es都指向显卡movbx,VIDEO_SEG_BEGINmovds, bxmoves, bx; 目前ax存放着光标的位置cmpcl, CHAR_RET; 判断字符是否是回车jne.next0; 不是回车则继续接下来的步骤.deal_ret: ; 是回车则处理回车movbl, 80divblmulbl; 除去光标位置中80的余数即可; ax中得到的是回车后光标的位置jmp.set_cursor.next0:cmpcl, CHAR_NL; 判断是否是换行符jne.next1; 如果不是换行符则继续接下来的代码.deal_nl: ; 处理换行的情形addax, 80; 换行很简单,只要加80即可jmp.deal_roll_screen; 换行可能会造成屏幕滚动,因此需要处理.next1:; 结束、回车、换行都不是那就是普通字符了,因此需要打印出来,并且光标后移一位movbx, ax; 先将ax复制到bx中shlbx, 1; 显卡区域每个字符占两个字节(还有一个属性字节)mov[bx], clincax; 光标后移一位; jmp.deal_roll_screen; 光标后移也可能会造成滚屏.deal_roll_screen:cmpax, 2000jl.set_cursor; 检查光标是否越界,如果越界则需要滚屏,否则可以直接设置光标.roll_screen: ; 滚屏处理movsi, 80 * 2movdi, 0movcx, 2000 - 80cldrepmovsw.clear_bottom_line: ; 滚屏后需要清除最后一行movbx, (2000 - 80) * 2movcx, 80.cls:movword [bx], DCHAR_NONEaddbx, 2loop.clsmovax, 2000 - 80; 滚屏后光标位置设置成最后一行起始; jmp.set_cursor; 滚屏完成后方可显示新的光标的位置了.set_cursor:movbx, ax; 将光标位置备份到bx中,因为访问端口会用到axmovdx, PORT_CHOOSEmoval, SUBPORT_HIGHoutdx, almovdx, PORT_DATAmoval, bhoutdx, almovdx, PORT_CHOOSEmoval, SUBPORT_LOWoutdx, almovdx, PORT_DATAmoval, bloutdx, alpopbxpopdsret; section code1 end;;;;section code2 align=16 vstart=0_start:.begin:pushword [es:seg_addr_code1]; code2没做什么实事就是再跳回code1的continue继续执行movax, start.continuepushaxretf; section code2 end;;;;section trail align=16app_end:; section trail end
!resb指令就是reserve byte的缩写,即保留一定数量的字节的意思,因此必然还有resw、resd,表示保留一定数量的字和双字的意思,既然是保留就不对定义的数据进行初始化,因此该指令就是定义一段连续的未初始化的数据;

!关于汇编地址的介绍以及硬盘访问的端口都在源代码中详细介绍,所以这里就不累述了;


加载器的实现:作为主引导扇区程序

loader.nas,编译后生成loader.mbr

; 主引导扇区程序作为应用程序加载器; 虽然就只有一个段但是也需要定义; 最主要是为了使用段属性vstart=0x7C00; 这样就可以使得段内的所有汇编地址都是相对0x7C00开始的; 因为MBR加载在0x0000:0x7C00处,因此IP初始化为0x7C00; 而所有偏移地址都是相对0x7C00的; 有了这一步程序中的所有标号都能真正代表偏移地址了section loader align=16 vstart=0x7C00jmpnear startLBA_APP_STARTequ100; 应用程序所在硬盘的起始逻辑扇区号,这里是人为规定的ADDR_20_LOAD_STARTdd0x10000; 内存中加载的起始20位绝对物理地址; 应用程序头中信息的偏移地址APP_SIZE_LOWequ0x00APP_SIZE_HIGHequ0x02APP_ENTRYequ0x04APP_ENTRY_SEGequ0x06APP_ENTRY_SEG_LOWequ0x06APP_ENTRY_SEG_HIGHequ0x08C_REALLOC_TBLequ0x0ATBL_STARTequ0x0C; 从0x0FFFF往下(即地址减小)的一段区域一般都作为MBR的栈!; 因此ss:sp指向0x0000:0x0000; 这样在push的时候sp能回到0xFFFFstart:movax, 0movss, axmovsp, ax; ds -> 内存中加载的起始位置段地址movax, [cs:ADDR_20_LOAD_START]movdx, [cs:ADDR_20_LOAD_START+2]movbx, 16divbxmovds, axmoves, ax; 留给用户程序时使ds和es都指向加载位置首部; 先读取一个扇区,即应用程序头所在的扇区xordi, dimovsi, LBA_APP_START; [di:si]全局保存当前读取的逻辑扇区号movcx, 1; 读取一个扇区callread_lba; 读取完毕,ds:0指向程序的第一扇区中的内容movdx, [APP_SIZE_HIGH]movax, [APP_SIZE_LOW]movbx, 512divbxcmpdx, 0jne.deal_left; 有余数,可以将已经读取的那个扇区看做余数的扇区decax; 无余数则需要减去已经读取的那个扇区.deal_left:cmpax, 0jeredirect_entry; 如果没有剩余扇区要读则直接去重定位程序入口点pushds; 备份并改变其指向movcx, ax; 剩余要读的扇区数量movax, dsaddax, 0x20; 使其指向下一个512字节起始处(必然是16位对齐的)movds, axincsi; 指向下一个要读的扇区callread_lbapopds; 恢复ds使其指向加载的程序的开始处; 到此为止程序彻底加载完毕; 接下来的工作是将程序头中的入口地址,以及重定位表中的地址; 修改成实际的物理地址; 这里所重定位的地址都是段地址; 将程序中段的绝对汇编地址更新成加载在内存中的实际物理段地址; 公式是:16位物理段地址 = (整个程序起始位置的20位物理 + 段的32位绝对汇编地址) >> 4redirect_entry: ; 重定位入口处地址movdx, [APP_ENTRY_SEG_HIGH]; [dx:ax]中保存入口处的绝对汇编地址movax, [APP_ENTRY_SEG_LOW]callcalc_seg_phy_addr_16; 计算段的16位段地址(即物理段地址),结果保存在ax中mov[APP_ENTRY_SEG], ax; 更新; 处理重定位表movcx, [C_REALLOC_TBL]movbx, TBL_START.realloc:movdx, [bx + 2]movax, [bx]callcalc_seg_phy_addr_16mov[bx], axaddbx, 4loop.reallocjmpfar [APP_ENTRY]; 控制权交给应用程序; func read_lba; <- [di:si]:读取的逻辑扇区号; <- cx:读取的扇区数量; <- ds:目的区域段地址; 将cx个扇区的内容读取到ds:0所指向的内存空间中read_lba:PORT_DATAequ0x1F0; 数据端口(16位)PORT_ERRNOequ0x1F1; 错误端口(8位)保存最后一次执行命令后的状态(错误原因)PORT_CLBAequ0x1F2; 计数端口(8位)保存读写的扇区数量PORT_LBA_STARTequ0x1F3; 逻辑扇区号端口(32位共4个8位口); 低28位确定待操作的起始扇区号; 最高的4位指定扇区寻址模式以及类型选择符)PORT_CTRLequ0x1F7; 控制端口(8位)下读写命令同时又能反映硬盘工作状态CTRL_READequ0x20; 读命令,向控制端口发送BIT_MASKequ10001000B; 位掩码,取控制端口的第7位和第3位; 第7位表示硬盘是否忙,1表示忙; 第3位表示硬盘是否就绪,1表示就绪STATUS_READYequ00001000B; 彻底就绪时第7位是0,第3位是1,用于检测硬盘是否就绪; 指定读取的扇区数量movdx, PORT_CLBAmoval, cloutdx, al; 向LBA地址口写入28位逻辑扇区号movdx, PORT_LBA_START; 0~7位movax, sioutdx, alincdx; 8~15位moval, ahoutdx, alincdx; 16~23位movax, dioutdx, alincdx; 24~27位moval, 111_0_0000B; ah保存24~27位,al中保存扇区寻址模式以及类型选择符; 其中最高位的111表示采用28位逻辑扇区号模式; 后面一位的0表示是主盘,1表示从盘,即盘片类型选择符oral, ahoutdx, al; 发出读命令movdx, PORT_CTRLmoval, CTRL_READoutdx, al.waits:; 检测硬盘是否就绪,没就绪就一直等待就绪inal, dxandal, BIT_MASKcmpal, STATUS_READYjne.waits; 准备就绪就开始读取shlcx, 8; 一个扇区512B,即256个字; cx记录剩余多少字未读完,而cx原本存放剩余扇区数; 因此cx要乘以256,即左移8位movdx, PORT_DATAxorbx, bx.readw: ; 循环读取程序,将其加载至ds:0处inax, dxmov[bx], axaddbx, 2loop.readwret; func calc_seg_phy_addr_16; <- [dx:ax]:段32位绝对汇编地址; -> ax:16位物理段地址calc_seg_phy_addr_16:; 这里的20位起始加载地址使用32位保存的; 因此可以通过带进位的加法得到段起始位置的实际的20位物理地址addax, [cs:ADDR_20_LOAD_START]adcdx, [cs:ADDR_20_LOAD_START+2]; 现在将绝对的20位物理地址右移4位就能得到16位的物理段地址了; 必须dx和ax同时右移; 方法是ax右移4位即可; 而dx采用循环右移4位,应该移到ax高4位的那4位重新回到dx高4位; 然后用位掩码去的dx高4位; 再利用or将这4位写入ax的高4位即可shrax, 4; 低16位右移4位rordx, 4anddx, 0xF000; 位掩orax, dx; 写入rettimes 510-($-$$) db 0 dw 0xAA55
!注意:程序开始处的一大堆宏定义就是用于和应用程序头部进行通讯的,即那些表项等在头部中的偏移位置,这样就可以轻松访问这些表项了;


3. 查看程序运行结果:将loader.mbr写进虚拟硬盘的0号扇区(总共一个扇区),将app.bin写进100号扇区(总共两个扇区),然后将虚拟盘作为虚拟机的启动盘放在VirtualBox中运行中即可;


0 0