汇编语言-实现一个简单的主引导记录(MBR)引导用户程序

news2025/2/24 10:12:25

本文参考李忠老师的《X86汇编语言:实模式到保护模式》

前言

自己手动实现一个简单的主引导记录来引导用户程序,有助于了解

  • 主引导程序的工作流程
  • 在汇编代码层面如何调用函数(函数调用的原理)
  • 在汇编代码层面如何读写硬盘(CPU与外围设备的交互)

内容虽然不多,但能够综合运用到多方面的知识。

主引导记录

以下内容参考自wiki

主引导记录(Master Boot Record,缩写:MBR),又叫做主引导扇区,是计算机开机后访问硬盘时所必须要读取的首个扇区,它在硬盘上的三维地址为(柱面,磁头,扇区)=(0,0,1)

系统开机的过程:

  1. BIOS 加电自检,对系统硬件(包括内存)进行检查
  2. 读取主引导记录。BIOS 将磁盘第一个扇区(也就是 MBR 扇区)读入内存地址 0000:7C00H 处
  3. 根据 MBR 中的引导代码启动引导程序
  4. 引导程序加载操作系统内核

修改主引导记录

我们可以将自己写的机器指令写入到主引导扇区,这样 BIOS 自检后就会执行我们指定的程序。就像这样:

示例汇编代码 demo.asm:

    mov ax, 0x1234
    jmp $

;--------------------------------------------------------------
    ; $  表示当前行的汇编地址
    ; $$ 表示第一行的汇编地址
    ; $-$$得到前面代码占用的字节数
    times (512-2-($-$$)) db 0 ;主引导扇区总长度必须512字节,用0占位
    db 0x55, 0xaa           ;主引导扇区最后两个字节必须是0x55aa

使用 nasm 进行编译得到 demo.bin 文件

用李忠老师的虚拟硬盘写入工具 Vhd writer 将 demo.bin 文件中的内容写入到 Vhd 虚拟硬盘的主引导扇区中

启动 bochsdbg 虚拟机进行调试,因为主引导记录将被加载到物理地址 0x7c00 处,我们使用命令b 0x7c00c连续执行指令,直到 ip 寄存器指向0x7c00时暂停运行

在这里插入图片描述

使用s命令进行单步调试,使用r命令观察寄存器的值

在这里插入图片描述

主引导记录引导用户程序

我们自己编写主引导程序,就可以让其加载其他内容(将控制权让出),比如某个用户程序。要加载用户程序,有一些必要的信息要给主引导程序:用户程序的总长度、程序的执行入口等。因此,就如同计算机世界中的各种协议头部一样,在用户程序的最前面,需要加上一个头部段。

头部段包含以下内容:

  1. 程序的总长度,占一个 double word,也即 4B
  2. 入口点段内偏移地址,占一个 word,也即 2B
  3. 入口点段汇编地址,占一个 double word,也即 4B
  4. 重定位表项数,占一个 word,也即 2B
  5. 与之对应的n项表项,每个表项占一个double word,也即 4B

在这里插入图片描述

用户程序代码示例代码:

;------------------------------------------------------
SECTION head_section vstart=0
                                                            ;用户程序头部段
    app_length: dd app_end
    entry_offset: dw start                                  ;用户程序入口点段内偏移地址
    entry_sec_addr: dd section.code_section.start           ;用户程序入口点段汇编地址
    realloc_tbl_num: dw (head_sec_end-code_sec_base_addr)/4 ;每项4字节
    code_sec_base_addr: dd section.code_section.start       ;代码段汇编地址
    data_sec_base_addr: dd section.data_section.start       ;数据段汇编地址
    stack_sec_base_addr: dd section.stack_section.start     ;栈段汇编地址
    head_sec_end:
;------------------------------------------------------
SECTION code_section align=16 vstart=0                      ;代码段

put_string:
                                                            ;input: ds:si 源字符串起始地址
                                                            ;        es:di 目标位置起始地址
    push ax                                                 ;保存寄存器
@1:
    mov al,[ds:si]
    cmp al,0                                                ;字符串结尾
    je end_loop
    mov ah,0x0f                                             ;字体格式
    mov [es:di],ax
    add di,2
    inc si
    jmp @1
end_loop:
    pop ax                                                  ;恢复寄存器
    ret

start:
                                                            ;设置用户程序的栈空间
    xor ax,ax
    mov sp,ax
    xor ax,[stack_sec_base_addr]
    mov ss,ax
                                                            ;程序主体部分,显示数据段文本
    mov ax,[data_sec_base_addr]
    mov ds,ax
    xor si,si
    mov ax,0xb800
    mov es,ax
    xor di,di
    call put_string

;------------------------------------------------------
SECTION data_section align=16 vstart=0                      ;数据段
    db 'Hello World! ___Written by cy.',0

;------------------------------------------------------
SECTION stack_section align=16 vstart=0                     ;栈段
    resb 256

;------------------------------------------------------
SECTION end_section
    app_end:

有了头部段,我们将用户程序读入到内存中,并将控制权交给它(修改段地址寄存器),就完成了加载工作。简单起见,我们做以下约定:

  1. 用户程序在磁盘第 100 个扇区中顺序存储
  2. 主引导程序始终将用户程序加载到物理内存 0x10000 处

读写硬盘

CPU与外围的 I/O设备传输数据需要借助 I/O接口,I/O接口内部集成了一些寄存器,我们称为端口,不同的端口有不同的作用,比如发送命令、发送数据等。CPU 从端口读写数据,与外围设备进行交互。我们用inout指令从端口读写数据:

in指令:

    ;一般使用ax和dx寄存器
    ;选择ax还是al取决于端口的位宽
    mov dx,0xc30    ;从0x3c0端口读取数据
    in ax,dx
    ;in al,dx

out指令:

    mov dx,0xc30    
    out dx,ax       ;往0x3c0端口写入数据
    ;out dx,al

在我们的计算机中,硬盘接口拥有八个端口,依次是 0x1f0 至 0x1f7,要读写的端口号存放在 dx 中,数据存放在 ax/al 中

读写硬盘的最小单位是扇区,因此硬盘是典型的块设备。读写硬盘有两种模式:

  1. CHS模式(Cylinder Head Sector)柱面-磁头-扇区三元组
  2. LBA模式(Logic Block Address)逻辑扇区号

读取硬盘的基本步骤:

  1. 将要读取的扇区数量写入 0x1f2 端口
    mov dx,0x1f2
    mov al,0x1
    out dx,al
  1. 将选择的硬盘(主/从)、读写模式(CHS/LBA)以及扇区号共32位数据写入0x1f3、0x1f4、0x1f5、0x1f6端口
1010 0000 00000000 00000000 00000000
共写入32位,低28位表示扇区号
第29和31位固定为1
第28位为0表示读写主硬盘,为1表示读写从硬盘
第30位为0表示CHS读写模式,为1表示LBA读写模式在·
    mov dx,0x1f2
    mov al,0x2
    out dx,al;0000 0002

    inc dx 
    xor al,al
    out dx,al;0000 0000

    inc dx 
    out dx,al;0000 0000
    inc dx 

    inc dx 
    mov al,0xe0
    out dx,al;1110 0000
    inc dx 
    ;11100000 00000000 00000000 00000002
    ;选择LBA读写模式,读写主硬盘的2号逻辑扇区
  1. 向端口 0x1f7 写入 0x20 命令,请求读硬盘
    mov dx,0x1f7
    mov al,0x20
    out dx,al
  1. 判断硬盘是否已经准备好被读取

从 0x1f7 端口读取 1B 数据

    mov dx,0x1f7
    in al,dx
00000000
共读取8位
第7位为1表示硬盘正忙
第3位为1表示硬盘已经可以交换数据

轮询等待,直到可以读取数据

    mov dx,0x1f7
.waits:
    in dx,al
    and al,0x88     ;取出第3位和第7位
    cmp al,0x08
    jnz .waits
  1. 硬盘准备好被读取后,从数据端口 0x1f0 读取数据
    ;假定DS:BX已经指向数据要存放的逻辑地址
    mov dx,0x1f0
    mov cx,256      ;512B=256word
.readlo:
    in dx,ax
    mov [bx],ax  
    add bx,2
    loop .readlo

加载用户程序

明白怎么读取硬盘后,接下来的目标是加载整个用户程序。首先要将用户程序头部段加载入内存中,并解析头部段中的信息。

我们在前面已经做好如下约定:

  1. 用户程序在磁盘第 100 个扇区中顺序存储
  2. 主引导程序始终将用户程序加载到物理内存 0x10000 处

因此,我们将逻辑扇区号定义为常数,而物理地址因为后面需要访问,因此存放到一个4字节内存中:

    disk_lba_address equ 100        ;定义常数,约定的逻辑扇区号
    phy_base_address : dd 0x10000   ;约定的用户程序要存放的物理地址

接下来,我们先将第 100 号扇区(包含了头部段)读入内存,并根据头部段中的第一个双字——程序的总长度来读入剩余的全部扇区

具体步骤如下:

  1. 读入第 100 号扇区(包含了头部段),并解析程序的总长度。一个扇区512B,总长度/512就是扇区个数,需要注意的是,无法整除时,还需要一个扇区。
    mov ax,[cs:phy_base_address]
    mov dx,[cs:phy_base_address+2]
    mov bx,16
    div bx                          ;左移四位得到逻辑段地址
    mov ds,ax                       ;令ds指向将要载入的逻辑段地址
    xor bx,bx                       ;对应read_hard_disk的输入

    mov si,disk_lba_address         ;对应read_hard_disk的输入
    xor di,di

    call read_hard_disk             ;读出头部段

    mov ax,[0x00]                   ;dx:ax = 程序总长度
    mov dx,[0x02]
    mov bx,512
    div bx                          ;得到用户程序占用的扇区数
    cmp dx,0
    jz @1                           ;占用整数个扇区
    inc ax                          ;不是刚好占用整数个扇区,需要继续读取一个扇区才能读完
@1:
    dec ax                          ;已经读了一个扇区,减去
  1. 使用loop循环来读取剩余扇区
    mov cx,ax                       ;需要读取的扇区数量
@2:
    mov ax,ds;
    add ax,0x20                     ;0x20<<4=0x200=512B,得到下一个512B的逻辑段地址
    mov ds,ax
    xor bx,bx
    inc si                          ;读下一个逻辑扇区
    call read_hard_disk
    loop @2                         ;循环读取剩余扇区

用户程序的重定位

我们已经成功的将整个用户程序加载入内存之中,接下来的任务是将控制权交给用户程序,显然需要修改 cs:ip 寄存器为用户程序入口点的逻辑地址。

如何计算呢?在头部段中有两个信息:

  1. 用户程序的入口点 段汇编地址。要通过该值计算出 cs,也即段基地址的值,其实很简单,用户程序被加载的起始物理地址是 phy_base_address,只需两者相加就是用户程序入口点的段实际物理地址,左移四位即得到段基地址 cs 的值

  2. 用户程序的入口点 段内偏移地址。直接对应了 ip 的值

计算出结果后,为了后续使用方便,我们直接将结果回填到 头部段中 用户程序的入口点段汇编地址 处。很“巧合”的是,在头部段偏移地址为 [0x04] 处存放的是 ip 的值,在头部段偏移地址为 [0x06] 处存放的是 cs 的值,直接使用jmp far指令即可跳转到用户程序入口点执行。

    jmp far [0x04]

在用户程序中,还有多个SECTION,如果不知道它们的段基地址,就难以使用。这时,重定位表项就派上用场了——我们按照上面的计算过程,将重定位表项中所有SECTION的段基地址计算出来,并一一回填到对应的内存空间中,这样,要访问这些SECTION的时候,只需要将段基地址设置为对应的值即可。

                                    ;计算用户程序入口点逻辑段地址
    mov ax,[0x06]
    mov dx,[0x08]
    call cal_segment_base
    mov [0x06],ax                   ;将计算得到的用户程序入口点段基地址回填到头部段[0x06]处

    mov cx,[0x0a]                   ;重定位表项数
    mov bx,[0x0c]                   ;第一项
@3:
    mov ax,[bx]
    mov dx,[bx+0x02]
    call cal_segment_base
    mov [bx],ax                     ;将计算得到的段基地址回填到重定位表项处
    add bx,4
    loop @3

主引导程序拿回控制权【扩展】

用户程序执行完之后,肯定需要将控制权交回给上级,否则只能运行一个程序了,原理很简单,只需保存各个寄存器的值并恢复即可,但是对于cs/ip/ss/sp寄存器的保存和恢复需要特别小心,比如修改了cs,但是ip还没修改,这时候会直接指向其他指令(cs:ip改变了)。

另外, ip寄存器的值不能直接读写,还记得 call指令吗?它的原理是将 ip寄存器的值压入栈,因此我们可以通过 call指令间接得到 ip寄存器的值,就像这样:

    push cs                         
    call get_ip                     ;等价于push ip
get_ip:
    pop ax                          ;得到ip的值
    push ax

看似天衣无缝了,不过这样保存的 ip是不对的哦,因为此时的 ip指向的是指令pop ax的起始地址,显然,我们想要的是push ax之后那条指令的起始地址。类似于这样的问题还有很多,动手实践的过程中就会遇到。

主引导程序拿回控制权代码(主引导程序部分):

    push ax                         ;保存环境
    push bx
    push cx
    push dx
    push ds
    push es
    push di
    push si

    push cs                         ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
    call get_ip                     ;等价于push ip
get_ip:
    pop ax                          ;得到ip的值
    add ax,delta_2-get_ip
    push ax

    jmp far [0x04]                  ;==执行用户程序==
delta_2:                            ;【拓展内容】主引导程序拿回控制权之后应该从此处开始执行
    pop si
    pop di
    pop es
    pop ds
    pop dx
    pop cx
    pop bx
    pop ax                         ;恢复环境

主引导程序拿回控制权代码(用户程序部分):

mov si,ss                                               ;【拓展内容】记录主引导程序的栈空间
    mov di,sp

                                                            ;设置用户程序的栈空间
    xor ax,ax
    mov sp,ax
    xor ax,[stack_sec_base_addr]
    mov ss,ax

    push di
    push si                                                 ;【拓展内容】记录主引导程序的栈空间

    ;用户程序代码部分
    ;..............
    ;..............
    ;用户程序代码部分


    pop si ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
    pop di
    mov ss,si
    mov sp,di
    retf ;= pop ip & pop cs

However,总感觉控制权需要用户程序主动配合交出怪怪的… …

验证

主引导程序完整汇编代码 my_mbr.asm:

    disk_lba_address equ 100        ;定义常数,约定的逻辑扇区号
SECTION mbr vstart=0x7c00
                                    ;初始化栈寄存器
    xor ax,ax
    mov ss,ax
    mov sp,ax

    mov ax,[cs:phy_base_address]
    mov dx,[cs:phy_base_address+2]
    mov bx,16
    div bx                          ;左移四位得到逻辑段地址
    mov ds,ax                       ;令ds指向将要载入的逻辑段地址

    xor di,di
    mov si,disk_lba_address         ;对应read_hard_disk的输入
    xor bx,bx                       ;对应read_hard_disk的输入
    call read_hard_disk             ;读出头部段

    mov ax,[0x00]                   ;dx:ax = 程序总长度
    mov dx,[0x02]
    mov bx,512
    div bx                          ;得到用户程序占用的扇区数
    cmp dx,0
    jz @1                           ;占用整数个扇区
    inc ax                          ;不是刚好占用整数个扇区,需要继续读取一个扇区才能读完
@1:
    dec ax                          ;已经读了一个扇区,减去
    cmp ax,0
    jz direct                       ;无扇区需要读了

    push ds                         ;保存用户程序的逻辑段地址

    mov cx,ax;
@2:
    mov ax,ds
    add ax,0x20                     ;0x20<<4=0x200=512B,得到下一个512B的逻辑段地址
    mov ds,ax
    xor bx,bx
    inc si                          ;读下一个逻辑扇区
    call read_hard_disk
    loop @2                         ;循环读取剩余扇区

    pop ds                          ;恢复用户程序的逻辑段地址

direct:
                                    ;计算用户程序入口点逻辑段地址
    mov ax,[0x06]
    mov dx,[0x08]
    call cal_segment_base
    mov [0x06],ax                   ;将计算得到的用户程序入口点段基地址回填到头部段[0x06]处

    mov cx,[0x0a]                   ;重定位表项数
    mov bx,0x0c                     ;第一项的地址
@3:
    mov ax,[bx]
    mov dx,[bx+0x02]
    call cal_segment_base
    mov [bx],ax                     ;将计算得到的段基地址回填到重定位表项处
    add bx,4
    loop @3

    push ax                         ;保存环境
    push bx
    push cx
    push dx
    push ds
    push es
    push di
    push si

    push cs                         ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
    call get_ip                     ;等价于push ip
get_ip:
    pop ax                          ;得到ip的值
    add ax,delta_2-get_ip
    push ax

    jmp far [0x04]
delta_2:                            ;【拓展内容】主引导程序拿回控制权之后应该从此处开始执行
    pop si
    pop di
    pop es
    pop ds
    pop dx
    pop cx
    pop bx
    pop ax                         ;恢复环境

    jmp $
;------------------------------------------------------------------------
cal_segment_base:
                                    ;input: dx:ax = 段汇编地址
                                    ;output:ax = 段基地址
    push dx

    add ax,[cs:phy_base_address]
    adc dx,[cs:phy_base_address+2]  ;带进位加法
                                    ;ds:ax=段逻辑地址,要取出第4-19位
    shr ax,4
    ror dx,4
    or ax,dx

    pop dx

    ret
;------------------------------------------------------------------------
read_hard_disk:
                                    ;从硬盘读入一个扇区
                                    ;input: DI:SI = 扇区号
                                    ;       DS:BX = 物理内存
    push di                         ;保存相关寄存器
    push si
    push ds
    push bx
    push ax
    push dx
    push cx

    mov dx,0x1f2
    mov al,1
    out dx,al

    inc dx
    mov ax,si                       ;写入扇区号、读写模式、硬盘
    out dx,al

    inc dx
    shr ax,8
    out dx,al

    inc dx
    mov ax,di
    out dx,al

    inc dx
    mov al,0xe0
    out dx,al

    inc dx                          ;发送请求读命令
    mov al,0x20
    out dx,al

.waits:                             ;轮询硬盘是否可读
    in al,dx
    and al,0x88
    cmp al,0x08
    jnz .waits

    mov dx,0x1f0                    ;开始读硬盘
    mov cx,256
.readlo:
    in ax,dx
    mov [bx],ax
    add bx,2
    loop .readlo

    pop cx
    pop dx
    pop ax
    pop bx
    pop ds
    pop si
    pop di                          ;恢复相关寄存器

    ret
;------------------------------------------------------------------------
    phy_base_address : dd 0x10000   ;约定的用户程序要存放的物理地址
    times (512-2-($-$$)) db 0       ;主引导扇区总长度必须512字节,用0占位
    db 0x55, 0xaa                   ;主引导扇区最后两个字节必须是0x55aa

用户程序完整汇编代码 my_app.asm:

;------------------------------------------------------
SECTION head_section vstart=0
                                                            ;用户程序头部段
    app_length: dd app_end
    entry_offset: dw start                                  ;用户程序入口点段内偏移地址
    entry_sec_addr: dd section.code_section.start           ;用户程序入口点段汇编地址
    realloc_tbl_num: dw (head_sec_end-code_sec_base_addr)/4 ;每项4字节
    code_sec_base_addr: dd section.code_section.start       ;代码段汇编地址
    data_sec_base_addr: dd section.data_section.start       ;数据段汇编地址
    stack_sec_base_addr: dd section.stack_section.start     ;栈段汇编地址
    head_sec_end:
;------------------------------------------------------
SECTION code_section align=16 vstart=0                      ;代码段
put_string:
                                                            ;input: ds:si 源字符串起始地址
                                                            ;        es:di 目标位置起始地址
    push ax                                                 ;保存寄存器
@1:
    mov al,[ds:si]
    cmp al,0                                                ;字符串结尾
    je end_loop
    mov ah,0x0f                                             ;字体格式
    mov [es:di],ax
    add di,2
    inc si
    jmp @1
end_loop:
    pop ax                                                  ;恢复寄存器
    ret

start:
    mov si,ss                                               ;【拓展内容】记录主引导程序的栈空间
    mov di,sp
                                                            ;设置用户程序的栈空间
    xor ax,ax
    mov sp,ax
    xor ax,[stack_sec_base_addr]
    mov ss,ax

    push di
    push si                                                 ;【拓展内容】记录主引导程序的栈空间

    mov ax,[data_sec_base_addr]                             ;程序主体部分,显示数据段文本
    mov ds,ax
    xor si,si
    mov ax,0xb800
    mov es,ax
    xor di,di
    call put_string

    pop si                                                  ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
    pop di
    mov ss,si
    mov sp,di
    retf                                                    ;= pop ip & pop cs

;------------------------------------------------------
SECTION data_section align=16 vstart=0                      ;数据段
    db 'Hello World! ___Written by cy.',0

;------------------------------------------------------
SECTION stack_section align=16 vstart=0                     ;用户栈段
    resb 256                                                ;为栈空间预留256B

;------------------------------------------------------
SECTION end_section
    app_end:

分别将他们编译,生成 my_mbr.asm 和 my_app.asm,使用李忠老师的虚拟硬盘写入工具将它们写入到虚拟硬盘

在这里插入图片描述

执行virtual box虚拟机,可以看到正常显示期望的文本内容,证明我们的主引导程序正确的引导了用户程序。

在这里插入图片描述

还可以利用Bochsdbg进行调试,观察程序的执行过程,尤其是控制权来回切换时,各个寄存器以及栈空间的状态变化。

小结

真实的主引导程序引导操作系统肯定没这么简单,不过把这个实验写下来也能对相关过程有个大致的了解。与纸上谈兵相比,实践环节会遇到很多细节问题,我本以为一天就能完成这个实验,结果花了三四天,有些问题是由于没有彻底理解之前所学的知识,有些知识理解了,但在使用时又疏忽了。学技术,多实践,才能做到查漏补缺和加深记忆的效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/193922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android中级——滑动分析

SrcollAndroid坐标系视图坐标系常见方法实现滑动layout()offsetLeftAndRight()和offsetTopAndBottom()LayoutParamsscrollTo()与scrollBy()ScrollerVierDragHeplerAndroid坐标系 将屏幕左上角的顶点作为Android坐标系的原点&#xff0c;向右为X轴正方向&#xff0c;向下为Y轴正…

uni-app中uni-ui组件库的使用

介绍uni-ui是DCloud提供的一个跨端ui库&#xff0c;它是基于vue组件的、flex布局的、无dom的跨全端ui框架。uni-ui不包括基础组件&#xff0c;它是基础组件的补充特点高性能&#xff08;自动差量更新数据&#xff0c;优化逻辑层和视图层通讯折损&#xff0c;背景停止&#xff0…

Leetcode力扣秋招刷题路-0337

从0开始的秋招刷题路&#xff0c;记录下所刷每道题的题解&#xff0c;帮助自己回顾总结 337. 打家劫舍 III&#xff08;Mid&#xff09; 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口&#xff0c;我们称之为 root 。 除了 root 之外&#xff0c;每栋房子有且只有一…

ESP32+Arduino+OLED+u8g2播放视频

1、思路分析 ESP32采用Arduino开发&#xff0c;结合u8g2模块可以很方便地实现在oled上显示图片。因此&#xff0c;只需要将一个视频拆开成一帧帧&#xff0c;然后循环显示即可。 然而&#xff0c;有几个问题&#xff1a; 视频太大&#xff0c;esp32的flash无法存下怎么办&…

DynaSLAM-8 DynaSLAM中双目运行流程(Ⅱ):初始化SLAM系统部分System.cc

目录 1.回忆 2.System::System 1.回忆 上篇博客中我们讲述了DynaSLAM中初始化Mask R-CNN网络部分的代码。 这篇博客我们讲述初始化DynaSLAM除Mask R-CNN网络部分以外的代码。 2.System::System 初始化Mask R-CNN网络后&#xff0c;程序执行&#xff1a; // Create SLAM syst…

MongoDB 4.0支持事务了,还有多少人想用MySQL呢?

目录一、MongoDB 不支持事务&#xff1f;二、什么是事务&#xff1f;三、ACID的定义四、如何使用事务五、重要参数简介1、时间限制2、oplog大小限制六、连接池 数据库连接的缓存1、MongoDB查询数据五步走2、MongoDB连接池的参数配置七、聚合框架八、MongoDB文档格式设计1、限制…

【八大数据排序法】插入排序法的图形理解和案例实现 | C++

第十六章 插入排序法 目录 第十六章 插入排序法 ●前言 ●认识算法 ●一、插入排序法是什么&#xff1f; 1.简要介绍 2.图形理解 3.算法分析 ●二、案例实现 1.案例一 ●总结 前言 排序算法是我们在程序设计中经常见到和使用的一种算法&#xff0c;它主要是将…

MySQL【left join、right join、inner join】详细用法

参考链接&#xff1a;mysql的left join和inner join的详细用法https://blog.csdn.net/weixin_45906830/article/details/111133181 1. inner join&#xff1a;内连接&#xff1a;显示两个表中有联系的所有数据。 通俗讲&#xff1a;inner join 查找的数据是左右两张表共有的。 …

【C语言练习】字符串旋转你会嘛?

目录&#x1f36c;题目描述&#xff1a;&#x1f36d;思路一&#xff1a;&#x1f361;代码优化&#xff1a;&#x1f36d;思路二&#xff1a;&#x1f36c;题目描述&#xff1a;&#x1f36d;思路一&#xff1a;&#x1f36d;思路二&#xff1a;&#x1f36c;题目描述&#xf…

车辆控制器的 Fail Safe功能介绍

Fail Safe概要 在漆黑的夜路上&#xff0c;一辆开着头灯的汽车经过。 如果控制前照灯的控制器在这种情况下发生故障怎么办&#xff1f; 大灯会熄灭&#xff0c;造成危险吗&#xff1f; 不。 在这种情况下&#xff0c;控制器的“Fail Safe”被激活&#xff0c;前照灯保持其先前的…

企业需要一个数字体验平台(DXP)吗?

数字体验平台是一个软件框架&#xff0c;通过与不同的业务系统喝解决方案集成&#xff0c;帮助企业和机构建立、管理和优化跨渠道的数字体验。帮助企业实现跨网站、电子邮件、移动应用、社交平台、电子商务站点、物联网设备、数字标牌、POS系统等传播内容&#xff0c;除了为其中…

termux入门安装

下载安装 请使用F-Droid 的Termux&#xff0c;GooglePlay的 Termux 可能存在一些问题。 下载地址&#xff1a;https://f-droid.org/en/packages/com.termux/ 下载完成在安卓手机上直接安装Termux的apk文件就可以了。 termux换源 新版本的termux换源一条命令就可以超简单&…

【C++之类和对象】初识类和对象

目录前言一、面向对象VS面向过程二、类三、类的定义四、类的访问限定符五、封装六、C中的用struct和用class定义的类有何不同&#xff1f;七、类的作用域八、类的实例化九、计算类对象的大小十、this指针前言 C是一门面向对象的语言&#xff0c;之前学习的C语言是一种面向过程的…

对epoll的重新学习【附源码】

目录 一、概述 二、使用 三、API 3.1 epoll_create(int size) 3.2 epoll_ctl(int epfd,int op, int fd. struct epoll_event *event) 3.3 epoll_wait(int epfd, struct peoll_event *events, int maxevents, int timeout) 3.4 *ssize_t read(int fd, void buf, size_t c…

python模块之codecs

python 模块codecs python对多国语言的处理是支持的很好的&#xff0c;它可以处理现在任意编码的字符&#xff0c;这里深入的研究一下python对多种不同语言的处理。 有一点需要清楚的是&#xff0c;当python要做编码转换的时候&#xff0c;会借助于内部的编码&#xff0c;转换…

Spark读取Hive数据的两种方式与保存数据到HDFS

Spark读取Hive数据的两种方式与保存数据到HDFS Spark读取Hive数据的方式主要有两种 1、 通过访问hive metastore的方式&#xff0c;这种方式通过访问hive的metastore元数据的方式获取表结构信息和该表数据所存放的HDFS路径&#xff0c;这种方式的特点是效率高、数据吞吐量大、…

规则引擎-drools-4-动态生成drl文档

文章目录drools 引擎工作原理动态生成drl文件示例步骤模板文件 decision_rule_template.drt生成规则文件serviceDecisionNodeFact实体对象生成的drl字符串如下KieHealper 执行动态生成drl文件的原理实际应用过程中&#xff0c;很多时候&#xff0c;规则不是一成不变的&#xff…

54.Isaac教程--RealSense相机

RealSense相机 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 文章目录RealSense相机RealsenseCamera Codelet示例应用程序故障排除固件注意事项通过 USB 3.0 电缆使用 USB 3.0 端口x86_64 Linux 主机设置设置电源模型英特尔RealSense 435 摄像头…

分享159个ASP源码,总有一款适合您

ASP源码 分享159个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 159个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1EaQuRA6mxyylrNWLq8iKVA?pwdaljz 提取码&#x…

springmvc知识巩固

文章目录回顾spring知识前言什么是SpringMVCSpringMVC的优点SpringMVC的常用注解Controller注解的作用ResponseBody注解的作用SpringMVC重定向和转发SpringMVC主要组件SpringMVC的执行流程回顾spring知识 上篇整理了“spring知识巩固”常见面试题&#xff0c;有需要的伙伴请点…