1. 主引导扇区的作用以及开机之后的大致流程:

1) 为了学习实模式下的编程而不受操作系统的影响,因为在正常的开机后,经过主引导扇区的对操作系统的加载就会把计算机的控制权交给操作系统从而进入保护模式,因此就只有运行主引导扇区程序时系统处于实模式状态;

2) 内存逻辑地址空间:

i. 实模式下CPU有20根地址线,能访问的地址空间有1MB,但是这1MB并不全部都指向DRAM;

ii. 在体系结构中CPU将这1MB空间映射到了多个存储设备上,其中:

iii. ROM占据顶部64KB空间,F0000~FFFFF;

iv. DRAM占据底部640KB的1MB中最大的空间,00000~9FFFF;

v. 中间空出来320KB用于分配给外围设备的存储空间,其中最重要的就是字符界面的显示器,这是一种古老的显示模式,80(个字符) × 25(行)为一屏,一屏总共能满满显示2000个字符,由于每个字符包括其ASCII码和颜色等属性,因此一个字符占两个字节,因此一屏占4000个字节,而留个其的地址空间时B8000~BFFFF这32768个字节(即刚好一个段64KB),总共可以容纳8屏多一点点儿,而在屏幕上显示字符的过程仅仅就是向显存中存放字符的过程那么简单;

3) 开机加电后系统的运作流程:

i. 卡机加电后cs:ip自动指向0xFFFF0的BIOS固件处;

ii. 该处只有一条指令:jmp far 0xF000:0x0000,然后跳转到了BIOS固件的起始位置,运行BIOS的程序;

iii. 在BIOS程序中会先在0x00000处加载BIOS自己的中断向量表,然后再执行一些硬件初始化和自检程序;

iv. 完成自检和初始化后调用BIOS的int19中断例程将控制权交给操作系统(实质上该例程是读取磁盘0号逻辑扇区的MBR主引导扇区程序,而传统上MBR是属于操作系统的,虽然该程序仍然运行于实模式);

4) MBR:

i. 即主引导扇区程序,位于逻辑0号扇区内,作用是加载操作系统代码,使程序从实模式变换到保护模式,真正将系统的实权交给操作系统,即扮演一个接力手的角色;

ii. int19BIOS例程会从磁盘的0面0道1扇区(也是逻辑0号扇区)的512B内容加载到0x0000:0x7C00处;

iii. BIOS判断该扇区是MBR的标准就是检测512字节的最后两个字节是否是0x55和0xAA,这是规范!如果检测符合规范,则执行跳转指令jmp far 0x0000:0x7C00转到主引导程序处执行操作系统引导工作,否则就会开机失败转去执行错误处理中断例程报告本次开机失败!

5) 在这里我们不介绍如何引导操作系统,只介绍一个简单的MBR程序,让其在屏幕上显示一定的内容,并学会如何使用BOCHS单步调试,这也是调试操作系统内核的基础;

 ; 此程序用于显示标号target的汇编地址的十进制形式; 注意,此时跳到了0x0000:0x7C00处; 因此cs=0x0000而ip=0x7C00jmp     startstack      times 20 db 0                       ; 定义一个栈,程序中需要使用
len_stack   equ $ - stack   VIDEO_SEG_BEGIN         equ     0xB800          ; 显存起始段地址
THIS_SEG                equ     0x7C00          ; 整个程序自成一段,起始偏移地址为0x7C00但起始汇编地址是0x0000str_info           db      'Label offset: '  ; 要显示的信息
len_str_info        equ     $ - str_info        ; 上面字符串的长度target    db  0start:; es指向显存,ds指向str_infomov      ax, VIDEO_SEG_BEGINmov      es, axmov       ax, cs                      ; 此时cs=0x0000mov       ds, ax; 显示字符串"Label offet: "mov       ah, 0007H                   ; 设置字的颜色属性,ASCII码存放在al中,颜色属性存放在ah中    mov     bx, 0mov        bp, 0mov        cx, len_str_info
.lp1:       mov     al, [THIS_SEG + str_info + bx]mov     [es:bp], axinc      bxadd       bp, 2loop   .lp1; 初始化栈段mov      ax, csmov       ss, axmov       sp, THIS_SEG + stack + len_stack; 将target分解成5位十进制数的ASCII码并倒序入栈mov     ax, target                  ; ax存放被除数axmov      bx, 10                      ; bx存放除数10mov       cx, 5                       ; 最多是个五位数,一位位分解
.lp2:       xor     dx, dx                      ; 将dx清零,被除数就成了dx:axdiv       bxadd       dl, 0x30                    ; 余数加0x30得到ASCII码mov        dh, 0004H                   ; 设置该数字的颜色push  dx                          ; 由于是倒序的,所以用栈反转一下loop    .lp2; 将倒序的5个十进制数顺序弹出到显存从而可以显示正确的顺序mov       cx, 5
.lp3:       pop     word [es:bp]add     bp, 2loop   .lp3mov     dh, 0004Hmov        dl, 'D'mov        [es:bp], dx                 ; 最后补一个后缀D表示十进制数mov     word [es: bp + 2], 0       ; 补一个空白字符冲马桶,将显存中的内容冲到显示器上jmp        $                           ; 死循环卡住程序times 510-($-$$)   db 0                        ; 填满剩余空间,总共512KBdw 0xAA55                    ; 最后两字节存放MBR结束符

2. 局部标号和全局标号:

1) 局部标号有一个句号"."作为前缀,而全局标号没有前缀;

2) 局部标号的作用是用来解决程序过长时标号的命名冲突问题,局部标号可以使得同一个名字的标号可以多次重复定义并不会产生命名冲突;

3) 局部标号的作用域:

i. 局部标号往往夹在两个离得最近的全局标号之间,而其作用域也就位于这两个全局标号之间了;

ii. 在作用域之外可以重复定义同名的局部标号;

iii. 在逻辑上局部标号”属于“前面的最近的全局标号,这种属于关系类似于C语言中的结构体和结构体成员之间的关系;

iv. 如果在作用域之内访问局部标号可以”直呼其名“,但是如果想在作用域之外访问某个局部标号就要使用和C语言访问结构体成员的一样的方式去访问那个局部标号了:

s1:          mov     ah, aladd       cx, bxjmp       s2.tag1         ; 作用域之外访问,所属全局标号.局部标号,否则会报错,提示没有定义该局部标号!nopnopsub       ax, 1
s2:         jmp     .tag1           ; 在作用域范围之内访问mov     ax, bxadd       ax, bx.tag1:    nopmov      bx, cx  

!!!注意:gdb拒绝在命令行中对局部标号设置断点(可能是gdb的一个bug吧!),因此只能通过外部工具比如Insight等调试工具进行设置(这些工具可以在任意一行上设置断点);

; 应用程序头
; 用于提供加载器相关加载信息
; 是应用程序规范的一部分
section header vstart=0app_size        dd  app_end                 ; [APP_SIZE:0x00] 程序的大小(字节)app_entry      dw  start                   ; [APP_ENTRY:0x04] 入口处偏移地址app_entry_seg dd  section.code1.start     ; [APP_ENTRY_SEG:0x06] 入口处段地址; section.段名.start是NASM提供的伪指令,用于段起始位置在源程序中的绝对汇编地址; 绝对汇编地址是指相对于整个源程序头的偏移量,而整个程序头的绝对汇编地址是0; 绝对汇编地址是一个32位无符号数,因此使用dd表示c_realloc_tbl  dw  (tbl_end - tbl_start) / 4       ; [C_REALLOC_TBL:0x0A] 重定位表表项数目
tbl_start:  ; [TBL_START:0x0C]seg_addr_code1    dd  section.code1.startseg_addr_code2   dd  section.code2.startseg_addr_data1   dd  section.data1.startseg_addr_data2   dd  section.data2.startseg_addr_stack   dd  section.stack.start
tbl_end:
; section header end;;
;;
section stack align=16 vstart=0resb 256
stack_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,0x0adb '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0adb '     xor dx,dx',0x0d,0x0adb '     xor ax,ax',0x0d,0x0adb '     xor cx,cx',0x0d,0x0adb '  @@:',0x0d,0x0adb '     inc cx',0x0d,0x0adb '     add ax,cx',0x0d,0x0adb '     adc dx,0',0x0d,0x0adb '     inc cx',0x0d,0x0adb '     cmp cx,1000',0x0d,0x0adb '     jle @@',0x0d,0x0adb '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0adb 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=0
start:mov       ax, [seg_addr_stack]mov     ss, axmov       sp, stack_endmov        ax, [seg_addr_data1]mov     ds, axmov       bx, msg0call    put_string                  ; 显示第一段信息; 在加载程序中将es指向header了push   word [es:seg_addr_code2]    ; 先将code2的偏移地址和段地址入栈mov     ax, _start.beginpush    axretf                              ; 利用retf修改cs:ip使其跳转至code2.continue:mov      ax, [es:seg_addr_data2]mov      ds, axmov       bx, msg1call    put_string                  ; 使ds:bx指向msg1并输出jmp        $
; end start; 字符串控制宏以及显卡光标端口宏
CHAR_TRAIL          equ     0x00        ; 字符串结束符
CHAR_RET            equ     0x0D        ; 回车符
CHAR_NL             equ     0x0A        ; 换行符
DCHAR_NONE          equ     0x0720      ; 显存中显示空的字PORT_CHOOSE           equ     0x3D4       ; 索引端口,用于选择子端口(8位)
SUBPORT_HIGH        equ     0x0E        ; 子端口号
SUBPORT_LOW         equ     0x0F        ; 这两个子端口分别存放光标位置的高位和低位
PORT_DATA           equ     0x3D5       ; 数据端口,存放选定的端口中的数据(8位)VIDEO_SEG_BEGIN      equ     0xB800      ; 显卡区域起始段地址; func put_string
; <- [ds:bx]:msg0
; colision register: es
; 将msg0打印至屏幕
put_string:push es; 获取当前光标位置保存在ax中mov       dx, PORT_CHOOSEmov      al, SUBPORT_HIGH        out     dx, al                  ; 选择一个子端口mov        dx, PORT_DATAin     al, dxmov       ah, al                  ; 从子端口中读取光标高位保存在ah中mov      dx, PORT_CHOOSEmov      al, SUBPORT_LOWout      dx, almov       dx, PORT_DATAin     al, dx                  ; 同理从子端口中读取光标低位保存在al中; 最终将整个结果保存在ax中; 目前ax存放着光标的位置.lp:  mov     cl, [bx]                    ; 读取一个字符保存在cl中cmp       cl, CHAR_TRAIL              ; 判断该字符是否是结束符je     .retcall    put_char                    ; 不是结束符就打印该字符inc        bx                          ; 继续读取下一个字符jmp      .lp.ret:    pop     esret; func put_char
; <- cl:当前读取的一个字符
; colision register: ds, bx
put_char:   push    dspush  bx                      ; 备份; ds和es都指向显卡mov     bx, VIDEO_SEG_BEGINmov      ds, bxmov       es, bx; 目前ax存放着光标的位置cmp     cl, CHAR_RET            ; 判断字符是否是回车jne      .next0                  ; 不是回车则继续接下来的步骤.deal_ret: ; 是回车则处理回车mov     bl, 80div       blmul       bl                      ; 除去光标位置中80的余数即可; ax中得到的是回车后光标的位置jmp        .set_cursor.next0:  cmp     cl, CHAR_NL             ; 判断是否是换行符jne       .next1                  ; 如果不是换行符则继续接下来的代码.deal_nl: ; 处理换行的情形add        ax, 80                  ; 换行很简单,只要加80即可jmp       .deal_roll_screen       ; 换行可能会造成屏幕滚动,因此需要处理.next1:  ; 结束、回车、换行都不是那就是普通字符了,因此需要打印出来,并且光标后移一位mov        bx, ax                  ; 先将ax复制到bx中shl     bx, 1                   ; 显卡区域每个字符占两个字节(还有一个属性字节)mov      [bx], clinc     ax                      ; 光标后移一位; jmp   .deal_roll_screen       ; 光标后移也可能会造成滚屏.deal_roll_screen:cmp     ax, 2000            jl      .set_cursor             ; 检查光标是否越界,如果越界则需要滚屏,否则可以直接设置光标.roll_screen: ; 滚屏处理mov        si, 80 * 2mov       di, 0mov        cx, 2000 - 80cldrep     movsw.clear_bottom_line: ; 滚屏后需要清除最后一行mov       bx, (2000 - 80) * 2mov      cx, 80.cls:mov      word [bx], DCHAR_NONEadd        bx, 2loop   .clsmov     ax, 2000 - 80       ; 滚屏后光标位置设置成最后一行起始; jmp .set_cursor         ; 滚屏完成后方可显示新的光标的位置了.set_cursor:mov      bx, ax              ; 将光标位置备份到bx中,因为访问端口会用到axmov     dx, PORT_CHOOSEmov      al, SUBPORT_HIGHout     dx, almov       dx, PORT_DATAmov        al, bhout       dx, almov       dx, PORT_CHOOSEmov      al, SUBPORT_LOWout      dx, almov       dx, PORT_DATAmov        al, blout       dx, alpop       bxpop       dsret
; section code1 end;;
;;
section code2 align=16 vstart=0
_start:.begin:  push    word [es:seg_addr_code1]        ; code2没做什么实事就是再跳回code1的continue继续执行mov     ax, start.continuepush  axretf
; section code2 end;;
;;
section trail align=16
app_end:
; section trail end
; 主引导扇区程序作为应用程序加载器; 虽然就只有一个段但是也需要定义
; 最主要是为了使用段属性vstart=0x7C00
; 这样就可以使得段内的所有汇编地址都是相对0x7C00开始的
; 因为MBR加载在0x0000:0x7C00处,因此IP初始化为0x7C00
; 而所有偏移地址都是相对0x7C00的
; 有了这一步程序中的所有标号都能真正代表偏移地址了
section loader align=16 vstart=0x7C00jmp      near startLBA_APP_START     equ     100             ; 应用程序所在硬盘的起始逻辑扇区号,这里是人为规定的ADDR_20_LOAD_START    dd      0x10000         ; 内存中加载的起始20位绝对物理地址; 应用程序头中信息的偏移地址APP_SIZE_LOW      equ     0x00        APP_SIZE_HIGH       equ     0x02APP_ENTRY           equ     0x04APP_ENTRY_SEG       equ     0x06APP_ENTRY_SEG_LOW   equ     0x06APP_ENTRY_SEG_HIGH  equ     0x08C_REALLOC_TBL       equ     0x0ATBL_START           equ     0x0C; 从0x0FFFF往下(即地址减小)的一段区域一般都作为MBR的栈!; 因此ss:sp指向0x0000:0x0000; 这样在push的时候sp能回到0xFFFF
start:      mov     ax, 0mov        ss, axmov       sp, ax; ds -> 内存中加载的起始位置段地址mov       ax, [cs:ADDR_20_LOAD_START]mov      dx, [cs:ADDR_20_LOAD_START+2]mov       bx, 16div       bxmov       ds, axmov       es, ax; 先读取一个扇区,即应用程序头所在的扇区xor       di, di                      mov     si, LBA_APP_START           ; [di:si]全局保存当前读取的逻辑扇区号mov      cx, 1                       ; 读取一个扇区call    read_lba                ; 读取完毕,ds:0指向程序的第一扇区中的内容mov      dx, [APP_SIZE_HIGH]mov      ax, [APP_SIZE_LOW]mov       bx, 512div      bxcmp       dx, 0jne        .deal_left              ; 有余数,可以将已经读取的那个扇区看做余数的扇区dec     ax                      ; 无余数则需要减去已经读取的那个扇区.deal_left:cmp       ax, 0je     redirect_entry          ; 如果没有剩余扇区要读则直接去重定位程序入口点push    ds                      ; 备份并改变其指向mov       cx, ax                  ; 剩余要读的扇区数量mov      ax, dsadd       ax, 0x20                ; 使其指向下一个512字节起始处(必然是16位对齐的)mov       ds, axinc       si                      ; 指向下一个要读的扇区call    read_lbapop     ds                      ; 恢复ds使其指向加载的程序的开始处; 到此为止程序彻底加载完毕; 接下来的工作是将程序头中的入口地址,以及重定位表中的地址; 修改成实际的物理地址; 这里所重定位的地址都是段地址; 将程序中段的绝对汇编地址更新成加载在内存中的实际物理段地址; 公式是:16位物理段地址 = (整个程序起始位置的20位物理 + 段的32位绝对汇编地址) >> 4redirect_entry: ; 重定位入口处地址mov       dx, [APP_ENTRY_SEG_HIGH]        ; [dx:ax]中保存入口处的绝对汇编地址mov       ax, [APP_ENTRY_SEG_LOW]call calc_seg_phy_addr_16            ; 计算段的16位段地址(即物理段地址),结果保存在ax中mov       [APP_ENTRY_SEG], ax             ; 更新; 处理重定位表mov     cx, [C_REALLOC_TBL]mov      bx, TBL_START.realloc:mov       dx, [bx + 2]mov        ax, [bx]call    calc_seg_phy_addr_16mov     [bx], axadd     bx, 4loop   .realloc;mov        ax, ds  ;mov        es, ax                          ; 使es初始化成加载的起始位置并交给应用程序处理jmp        far [APP_ENTRY]                 ; 控制权交给应用程序; func read_lba
; <- [di:si]:读取的逻辑扇区号
; <- cx:读取的扇区数量
; <- ds:目的区域段地址
; 将cx个扇区的内容读取到ds:0所指向的内存空间中
read_lba:PORT_DATA      equ     0x1F0       ; 数据端口(16位)PORT_ERRNO     equ     0x1F1       ; 错误端口(8位)保存最后一次执行命令后的状态(错误原因)PORT_CLBA     equ     0x1F2       ; 计数端口(8位)保存读写的扇区数量PORT_LBA_START equ     0x1F3       ; 逻辑扇区号端口(32位共4个8位口); 低28位确定待操作的起始扇区号; 最高的4位指定扇区寻址模式以及类型选择符)PORT_CTRL      equ     0x1F7       ; 控制端口(8位)下读写命令同时又能反映硬盘工作状态CTRL_READ      equ     0x20        ; 读命令,向控制端口发送BIT_MASK        equ     10001000B   ; 位掩码,取控制端口的第7位和第3位; 第7位表示硬盘是否忙,1表示忙; 第3位表示硬盘是否就绪,1表示就绪STATUS_READY    equ     00001000B   ; 彻底就绪时第7位是0,第3位是1,用于检测硬盘是否就绪; 指定读取的扇区数量mov       dx, PORT_CLBAmov        al, clout       dx, al; 向LBA地址口写入28位逻辑扇区号mov        dx, PORT_LBA_START      ; 0~7位mov       ax, siout       dx, alinc       dx                      ; 8~15位mov      al, ahout       dx, alinc       dx                      ; 16~23位mov     ax, diout       dx, alinc       dx                      ; 24~27位mov     al, 0xE0;mov        al, 111_1_0000B     ; ah保存24~27位,al中保存扇区寻址模式以及类型选择符or        al, ahout       dx, al; 发出读命令mov        dx, PORT_CTRLmov        al, CTRL_READout        dx, al.waits:   ; 检测硬盘是否就绪,没就绪就一直等待就绪in      al, dxand       al, BIT_MASKcmp     al, STATUS_READYjne     .waits; 准备就绪就开始读取shl        cx, 2mov        dx, PORT_DATAxor        bx, bx.readw: ; 循环读取程序,将其加载至ds:0处in      ax, dxmov       [bx], axadd     bx, 2loop   .readwret; func calc_seg_phy_addr_16
; <- [dx:ax]:段32位绝对汇编地址
; -> ax:16位物理段地址
calc_seg_phy_addr_16:; 这里的20位起始加载地址使用32位保存的; 因此可以通过带进位的加法得到段起始位置的实际的20位物理地址add      ax, [cs:ADDR_20_LOAD_START]adc      dx, [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位即可shr     ax, 4           ; 低16位右移4位ror       dx, 4and        dx, 0xF000      ; 位掩or      ax, dx          ; 写入rettimes 510-($-$$) db 0dw 0xAA55

[Intel汇编-NASM]主引导扇区程序介绍相关推荐

  1. 【OS学习笔记】二十 保护模式六:保户模式下操作系统内核如何加载用户程序并运行 对应的汇编代码之主引导扇区程序

    本汇编代码对应保户模式下操作系统内核如何加载用户程序并运行 的实际主引导扇区代码: 对应的内核代码在:内核代码 对应的用户程序代码在:用户程序代码 ;代码清单13-1;文件名:c13_mbr.asm; ...

  2. 【OS学习笔记】三十七 保护模式十:中断和异常的处理与抢占式多任务对应的汇编代码----主引导扇区代码

    本文是以下几篇文章对应的主引导扇区代码汇编代码: [OS学习笔记]三十四 保护模式十:中断和异常区别 [OS学习笔记]三十五 保护模式十:中断描述符表.中断门和陷阱门 [OS学习笔记]三十六 保护模式 ...

  3. 主引导扇区程序代码优化-2

    上一期的代码使用笨拙的手段,将字符传入到显卡里,如果要增加或减少字符,工作量就会很大,考虑到这点,汇编当然有更好的方式去实现了,那就是循环,这篇文章将详细介绍. 会使用到一些新的指令 cld, mov ...

  4. 【OS学习笔记】八 实模式:编写主引导扇区代码-另一种更高效的写法

    学习交流加 个人qq: 1126137994 个人微信: liu1126137994 学习交流资源分享qq群: 962535112 上一篇文章,我们用比较原始的方法编写了主引导扇区的代码.点击链接查看 ...

  5. linux 汇编 读取软盘,[Linux]dd 读写软盘:在软盘主引导扇区写入显示hello world的二进制代码数据...

    代码效果 在软盘主引导扇区写入显示 hello world 的二进制代码数据 命令行操作 第一步,格式化软盘,/dev/fd0是软盘的名字 $ sudo fdformat /dev/fd0 $ sud ...

  6. 硬盘主引导扇区汇编代码

    GitHub地址:https://github.com/yifengyou/X86-assembly-language-from-real-mode-to-protection-mode/blob/m ...

  7. 第5章 编写主引导扇区代码

    开机过程 一.在屏幕上显示文本 01.显卡和显存 每个字节表示三原色中的一个(红绿蓝) 两种模式|-文本模式|-图像模式两种模式的显存是分开的;文本模式下,显存的内容是文本的编码:图像模式下,显存的内 ...

  8. 第五章 编写主引导扇区代码

    本章的思路是,在本机上上写一段代码(这些代码的意义是往显存中写一些数据)-->编译成bin文件-->写入到vhd硬盘的引导扇区(即第一扇区,见第四章详述)-->开机从硬盘启动,从而执 ...

  9. 【OS学习笔记】六 实模式:编写主引导扇区代码

    上一篇文章学习了:计算机的启动过程(点击链接查看上一篇文章) 这篇文章学习记录为:编写主引导扇区代码. 参考:<X86汇编语言-从实模式到保护模式>-李忠.纯学习笔记,更详细内容请阅读正版 ...

最新文章

  1. 【 MATLAB 】使用 residuez 函数求 z 反变换的几个案例分析
  2. 机器学习中的常见问题—损失函数
  3. 设计模式学习笔记(9)——代理模式
  4. php管理智能dns,负载均衡之DNS轮询
  5. 100个最古老互联网域名 最久只有23年(附名单)
  6. TensorFlow tf.keras.losses.SparseCategoricalCrossentropy
  7. 数据结构:从插入排序到希尔排序
  8. 下一个主要AI平台是什么?苹果说:手机
  9. matlab firl,matlab 利用matlab工具箱函数fir1 联合开发网 - pudn.com
  10. 亲自动手写爬虫系列三、爬取队列
  11. 一小时学会使用SpringBoot整合阿里云SMS短信服务
  12. 人工智能 漆桂林_2020年CCF专委活动计划(预通过)
  13. oracle 自动备份压缩(windows下)
  14. Messaging——WebSocket
  15. Discuz中常用数据库操作
  16. 接口Mock详解及使用
  17. java操作word,自动填写word表格
  18. JAVA 接口 匿名函数
  19. ActiveX 控件打包
  20. 若依的路由是怎么跳转的?

热门文章

  1. Mac os区别_Photoshop和PS有什么区别
  2. 第七章-Python3中Web开发框架flask实现粉丝关注与取消关注功能
  3. 基于Mycat的多租户分库方案
  4. Android将网络url转换为base64
  5. 判断素数的快速算法 sqrt()
  6. 力撑国人动画电影《龙之谷·破晓奇兵》深圳瑞云科技包场观影
  7. 陌陌基于Kubernetes和Docker容器管理平台的架构实践
  8. python苹果手机照片导入电脑_如何从mac下的photos导出照片
  9. PDF怎么转换成Word?电脑必备的转换工具
  10. Alexa的排名机制