linux0.1x内核代码学习笔记-boot启动
linux0.11上电时把启动盘第1扇区bootsect.s的代码拷贝到0x7c00位置处,这段代码自己把自己拷贝到0x90000 这个位置然后开始执行,利用bios预先设置好的中断函数,把第2扇区setup程序拷贝到0x90200处,一共4个扇区。把第6扇区开始的240个扇区system代码读取到内存地址 0x10000处共120KB,整个操作系统的代码已经读取到内存了,然后再确定根文件设备保存到root_dev这个位置,跳转到setup程序运行。
SYSSIZE = 3000h ;// 指编译连接后system模块的大小。;// 这里给出了一个最大默认值。SETUPLEN = 4 ;// setup程序的扇区数(setup-sectors)值BOOTSEG = 07c0h ;// bootsect的原始地址(是段地址,以下同)INITSEG = 9000h ;// 将bootsect移到这里SETUPSEG = 9020h ;// setup程序从这里开始SYSSEG = 1000h ;// system模块加载到10000(64kB)处.ENDSEG = SYSSEG + SYSSIZE ;// 停止加载的段地址
; 设备号具体值的含义如下:
; 设备号=主设备号*256 + 次设备号(也即 dev_no = ( major <<8 ) + minor )
; (主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)
; 0x300 - /dev/hd0 - 代表整个第 1 个硬盘;
; 0x301 - /dev/hd1 - 第 1 个盘的第 1 个分区;
; …
; 0x304 - /dev/hd4 - 第 1 个盘的第 4 个分区;
; 0x305 - /dev/hd5 - 代表整个第 2 个硬盘盘;
; 0x306 - /dev/hd6 - 第 2 个盘的第 1 个分区;
; …
; 0x309 - /dev/hd9 - 第 2 个盘的第 4 个分区;
; 从Linux内核0.95版后已经使用与现在相同的命名方法了。
! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.
; 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!= 0)就直
; 接使用给定的设备。否则就需要根据 BIOS 报告的每磁道扇区数来确定到底使用/dev/PS0 (2,28)
; 还是 /dev/at0 (2,8)。
; 上面一行中两个设备文件的含义:
; 在 Linux 中软驱的主设备号是2(参见第 78 行的注释),次设备号 = type * 4 + nr,其中
; nr 为 0 - 3 分别对应软驱A、B、C 或D;type 是软驱的类型(2->1.2M 或 7->1.44M 等)。
; 因为 7*4 + 0 = 28,所以 /dev/PS0 (2,28)指的是1.44M A 驱动器,其设备号是 0x021c
; 同理 /dev/at0 (2,8)指的是1.2M A 驱动器,其设备号是0x0208。seg csmov ax,root_dev ; 取出 root_dev 的值,判断根设备号是否被定义or ax,axjne root_definedseg cs ; 取出 sectors 的值(每磁道扇区数);sectors = 15 则说明是 1.2MB 的驱动器;mov bx,sectors ; sectors = 18 则说明是 1.44MB 的软驱。因为是可引导的驱动器,所以是A驱。mov ax,#0x0208 ! /dev/PS0 - 1.2Mbcmp bx,#15je root_definedmov ax,#0x021c ! /dev/PS0 - 1.44Mbcmp bx,#18je root_defined
undef_root: ; 都不等于的情况下则进入死循环jmp undef_root
root_defined: ; 将检查过的设备号保存到 root_dev 中。seg csmov root_dev,ax! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
; 到此,所有程序都加载完毕,我们就跳转到被加载在 bootsect 后面的 setup 程序去。jmpi 0,SETUPSEG.....
sectors:.word 0msg1:.byte 13,10.ascii "Loading".org 506
; 表示下面语句从地址506(0x1FC)开始,所以swap_dev在启动扇区的第506开
; 始的2个字节中,root_dev在启动扇区的第508开始的2个字节中。
swap_dev:.word SWAP_DEV
root_dev:.word ROOT_DEV; 下面是启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用.它必须位于引
; 导扇区的最后两个字节中。
boot_flag:.word 0xAA55.text
endtext:
.data
enddata:
.bss
endbss:
bootsect程序总结:
- 读取setup程序到0x90200处,一共4个扇区2K
- 读取系统代码到0x10000处,一共240扇区120KB
- 检查文件设备类型把设备号写到root_dev这个位置(508-510字节+0x90000)
setup程序说明:
1.首先设置DS段寄存器为0x9000,去取硬件信息存到内存0x90000处共510字节
内存地址 |
字节 |
名称 |
描述 |
0x90000 |
2 |
光标位置 |
行,列 |
0x90002 |
2 |
扩展内存数 |
从1M开始计算 KB |
0x90004 |
2 |
显示页面 |
|
0x90006 |
1 |
显示模式 |
|
0x90007 |
1 |
字符列数 |
|
0x90008 |
2 |
||
0x9000A |
1 |
显示内存 |
|
0x9000B |
1 |
显示状态 |
|
0x9000C |
2 |
特性参数 |
|
....... |
|||
0x90080 |
16 |
硬盘参数表 |
第一个硬盘参数表 |
0x90090 |
16 |
硬盘参数表 |
第二个硬盘参数表 |
0x901FC |
2 |
根设备号 |
根文件系统设备号 |
2.关闭cpu中断,把system代码从0x10000移动到0x00000处,此时实模式的中断向量已经被system代码段覆盖。
! now we want to move to protected mode ...
; 这里开始,我们开始进入保护模式cli ! no interrupts allowed ! ; 这里开始不允许任何中断! first we move the system to it's rightful place
; 首先我们将system模块移到正确的位置。
; bootsect引导程序是将system模块读入到从0x10000(64k)开始的位置。由于当时假设
; system模块最大长度不会超过0x80000(512k),也即其末端不会超过内存地址0x90000,
; 所以bootsect会将自己移动到0x90000开始的地方,并把setup加载到它的后面。
; 下面这段程序的用途是再把整个system模块移动到0x00000位置,即把从0x10000到0x8ffff
; 的内存数据块(512k),整块地向内存低端移动了0x10000(64k)的位置。mov ax,#0x0000cld ! 'direction' = 0, movs moves forward ; 清方向位
do_move:mov es,ax ! destination segment ; es:di是目的地址(初始为0x0:0x0)add ax,#0x1000cmp ax,#0x9000 ; 已经把最后一段(从0x8000段开始的64KB)代码移动完.jz end_move ; 判断是否移动完成mov ds,ax ! source segmentsub di,disub si,simov cx,#0x8000 ; 移动 0x8000个字repmovswjmp do_move
3.加载lgdt和lidt,打开A20地址实现1M内存以上的寻址,设置8259芯片的中断向量地址为0x20-0x28,跳转到代码段描述符1开始执行开始执行,也就是系统代码段执行。
; 全局描述符表开始处.描述符表由多个8字节长的描述符项组成.这里给出了3个描述符项.
; 第1项无用,但须存在.第2项的系统代码段描述符,第3项是系统数据段描述符.gdt:.word 0,0,0,0 ! dummy ;第1个描述符,不用; 在GDT表 这里的偏移量是0x08.它是内核代码段选择符的值..word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb).word 0x0000 ! base address=0.word 0x9A00 ! code read/exec ; 代码段为只读,可执行.word 0x00C0 ! granularity=4096, 386 ; 颗粒度4K,32位.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb).word 0x0000 ! base address=0.word 0x9200 ! data read/write ; 数据段为可读可写.word 0x00C0 ! granularity=4096, 386 ; 颗粒度4K,32位; 下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数.前2字节的IDT 的限长,后4字节是idt表在线性地址
; 空间中的32位基地址.CPU要求在进入保护模式之前需设置IDT表,因此这里先设置一个长度为0的空表.
idt_48:.word 0 ! idt limit=0.word 0,0 ! idt base=0L; 加载全局描述符表寄存器 gdtr 的指令 lgdt 要求的 6 字节操作数。
gdt_48:.word 0x800 ! gdt limit=2048, 256 GDT entries ; 表限长2k,8个字节等于1个描述符,共有256项.word 512+gdt,0x9 ! gdt base = 0X9xxxx ; 0x90200 + gdt
setup.s代码段总结:
- 读取硬件参数覆盖以前的bootsect.s代码处(0x90000-0x90200)。
- 关闭中断,移动系统代码从0x10000到0x0000处(共拷贝内存512KB)。
- 设置idt gdt 打开A20地址实现1M以上寻址,初始化8259A中断向量基地址为0x20-0x28(因为cpu使用0x00-0x20中断为其他用处),跳转到系统代码运行。
中断请求号 |
中断号 |
用途 |
IRQ0 |
0x20(32) |
8253发出的100HZ时钟中断 |
IRQ1 |
0x21(33) |
键盘中断 |
IRQ2 |
0x22(34) |
连接从芯片 |
IRQ3 |
0x23(35) |
串行口2 |
IRQ4 |
0x24(36) |
串行口1 |
IRQ5 |
0x25(37) |
并行口2 |
IRQ6 |
0x26(38) |
软盘驱动器 |
IRQ7 |
0x27(39) |
并行口1 |
IRQ8 |
0x28(40) |
实时时钟 |
IRQ9 |
0x29(41) |
保留 |
IRQ9 |
0x2A(42) |
保留 |
IRQ9 |
0x2B(43) |
保留(网络接口) |
IRQ9 |
0x2C(44) |
PS/2鼠标接口 |
IRQ9 |
0x2D(45) |
数学协处理器中断 |
IRQ9 |
0x2E(46) |
硬盘中断 |
IRQ9 |
0x2F(47) |
保留 |
head.s程序说明:
1.head程序和系统c语言代码是分别编译链接到一起组成system模块,head在最前面占用大概25KB+184B字节空间,head程序首先初始化所有段寄存器,初始化堆栈地址指针,设置中断向量,设置gdt描述符,段限长有所改变,所以再次重新设置段寄存器清除段寄存器缓存,设置idt中断处理函数,统一设置成忽略中断打印函数,检查A20地址是否被打开(往0处写一个数再去0x100000处读取判断数值是否一致,不一致则A20已经打开)。
/** setup_idt** sets up a idt with 256 entries pointing to* ignore_int, interrupt gates. It then loads* idt. Everything that wants to install itself* in the idt-table may do so themselves. Interrupts* are enabled elsewhere, when we can be relatively* sure everything is ok. This routine will be over-* written by the page tables.*/
/** 下面这段是设置中断描述符表子程序setup_idt** 将中断描述符表idt设置成具有256个项,并都指向ignore_int中断门.然后加载中断描述符表寄存器(lidt指令)。* 真正实用的中断门以后再安装.当我们在其他地方认为一切都正常时再开启中断.该子程序将会被页表覆盖掉.*/
# 中断描述符表中的项虽然也是8字节组成,但其格式与全局表中的不同,被称为门描述符.它的0-1,6-7字节是偏移量,
# 2-3字节是选择符,4-5字节是一些标志.该描述符,共256项.eax含有描述符低4字节,edx含有高4字节.内核在随后
# 的初始化过程中会替换安装那些真正实用的中断描述符项。setup_idt:lea ignore_int, %edx # 将ignore_int的有效地址(偏移值)值->eax寄存器movl $0x00080000, %eax # 将选择符0x0008置入eax的高16位中.movw %dx, %ax /* selector = 0x0008 = cs */ # 偏移值的低16位置入eax的低16位中.此时eax含有门描述符低4字节的值。movw $0x8E00, %dx /* interrupt gate - dpl=0, present */ # 此时edx含有门描述符高4字节的值.lea idt, %edi # idt是中断描述符表的地址.mov $256, %ecx
rp_sidt:movl %eax, (%edi) # 将哑中断门描述符存入表中.movl %edx, 4(%edi)addl $8, %edi # edi指向表中下一项.dec %ecxjne rp_sidtlidt idt_descr # 加载中断描述符表寄存器值.ret
.align 4
ignore_int:pushl %eaxpushl %ecxpushl %edxpush %dspush %espush %fsmovl $0x10, %eax # 设置段选择符(使ds,es,fs指向gdt表中的数据段)mov %ax, %dsmov %ax, %esmov %ax, %fspushl $int_msgcall printk # 该函数在kernel/printk.c中popl %eaxpop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret # 中断返回
/** setup_gdt** This routines sets up a new gdt and loads it.* Only two entries are currently built, the same* ones that were built in init.s. The routine* is VERY complicated at two whole lines, so this* rather long comment is certainly needed :-).* This routine will beoverwritten by the page tables.*/
/** 设置全局描述符表项setup_gdt* 这个子程序设置一个新的全局描述符表gdt,并加载.该子程序将被页表覆盖掉.**/
setup_gdt:lgdt gdt_descr # 加载全局描述符表寄存器(内容已设置好)ret
# 下面是加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数.前2字节是gdt表的限长,后4字节是gdt表的
# 线性基地址.这里全局表长度设置为2KB字节(0x7ff即可),因为每8字节组成一个描述符项,所以表中共可有256项。
# 符号gdt是全局表在本程序中的偏移位置.gdt_descr:.word 256 * 8 - 1 # so does gdt (not that that's any.long gdt # magic number, but it works for me :^).align 8 # 按8(2^3)字节方式对齐内存地址边界.
idt: .fill 256, 8, 0 # idt is uninitialized # 256项,每项8字节,填0.# 全局表,前4项分别是空项(不用),代码段描述符,数据段描述符,系统调用段描述符,其中系统调用段描述符并没有
# 派用处,Linus当时可能曾想把系统调用代码专门放在这个独立的段中。同时还预留了252项的空间,用于放置所创
# 建任务的局部描述符(LDT)和对应的任务状态段TSS的描述符.
# (0-nul, 1-cs, 2-ds, 3-syscall, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)
gdt:.quad 0x0000000000000000 /* NULL descriptor */.quad 0x00c09a0000000fff /* 16Mb */ # 0x08,内核代码段最大长度16MB..quad 0x00c0920000000fff /* 16Mb */ # 0x10,内核数据段最大长度16MB..quad 0x0000000000000000 /* TEMPORARY - don't use */.fill 252, 8, 0 /* space for LDT's and TSS's etc */ # 预留空间.
2.检查协x87数学协处理器是否存在,芯片不存在,需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。
/** NOTE! 486 should set bit 16, to check for write-protect in supervisor* mode. Then it would be unnecessary with the "verify_area()"-calls.* 486 users probably want to set the NE (#5) bit also, so as to use* int 16 for math errors.*/
/** 注意!在下面这段程序中,486应该将位16置位,以检查在超级用户模式下的写保护,此后"verify_area()"* 调用就不需要了。486的用户通常也会想将NE(#5)置位,以便对数学协处理器的出错使用int 16。**/# 上面原注释中提到的486CPU中CR0控制器的位16是写保护标志WP,用于禁止超级用户级的程序向一般用户只读# 页面中进行写操作。该标志主要用于操作系统在创建新进程时实现写时复制方法。# 下面这段程序用于检查数学协处理器芯片是否存在# 方法是修改控制寄存器CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器# 芯片不存在,需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。movl %cr0, %eax # check math chipandl $0x80000011, %eax # Save PG,PE,ET/* "orl $0x10020,%eax" here for 486 might be good */orl $2, %eax # set MPmovl %eax, %cr0call check_x87jmp after_page_tables/** We depend on ET to be correct. This checks for 287/387.*/
/** 我们依赖于ET标志的正确性来检测287/387存在与否.**/
# fninit向协处理器发出初始化命令,它会把协处理器置于一个末受以前操作影响的已和状态,设置其控制字为默认值,
# 清除状态字和所有浮点栈式寄存器。非等待形式的这条指令(fninit)还会让协处理器终止执行当前正在执行的任何
# 先前的算术操作。
# fstsw指令取协处理器的状态字。
# 如果系统中存在协处理器的话,那么在执行了fninit指令后其状态字低字节肯定为0。check_x87:fninitfstsw %axcmpb $0, %al # 初始化状态字应该为0,否则说明协处理器不存在je 1f /* no coprocessor: have to set bits */movl %cr0, %eaxxorl $6, %eax /* reset MP, set EM */movl %eax, %cr0ret.align 4 # 按4字节方式对齐内存地址, 为了提高32位CPU访问内存中代码或数据的速度和效率# 两个字节值是80287协处理器指令fsetpm的机器码。其作用是把80287设置为保护模式。# 80387无需该指令,并且将会把该指令看作是空操作
1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ # 287协处理器码ret
3.设置页目录和页表,然后返回到main函数开始运行,页目录一共设置了4项,页表也设置了4项,映射了16M地址线性地址与物理地址16M相对应映射。这里可能没有16m物理地址,只是页表实现了映射而已。
/***** 为跳转到init/main.c中的main()函数作准备工作 *****/
# 前面3个入栈0值应该分别表示envp,argv指针和argc的值(main()没有用到)
# pushl $L6 压入返回地址
# pushl $main 压入main函数的入口地址
# 当head.s最后执行ret指令时就会弹出main()的地址
after_page_tables:pushl $0 # These are the parameters to main :-)pushl $0 # 这些是调用main程序的参数(指init/main.c).pushl $0pushl $L6 # return address for main, if it decides to.pushl $mainjmp setup_paging # 跳转至setup_paging
L6:jmp L6 # main should never return here, but# just in case, we know what happens.# main程序绝对不应该返回到这里,不过为了以防万一,所以# 添加了该语句。这样我们就知道发生什么问题了。
/** Setup_paging** This routine sets up paging by setting the page bit* in cr0. The page tables are set up, identity-mapping* the first 16MB. The pager assumes that no illegal* addresses are produced (ie >4Mb on a 4Mb machine).** NOTE! Although all physical memory should be identity* mapped by this routine, only the kernel page functions* use the >1Mb addresses directly. All "normal" functions* use just the lower 1Mb, or the local data space, which* will be mapped to some other place - mm keeps track of* that.** For those with more memory than 16 Mb - tough luck. I've* not got it, why should you :-) The source is here. Change* it. (Seriously - it shouldn't be too difficult. Mostly* change some constants etc. I left it at 16Mb, as my machine* even cannot be extended past that (ok, but it was cheap :-)* I've tried to show which constants to change by having* some kind of marker at them (search for "16Mb"), but I* won't guarantee that's all :-( )*/
/** 这个子程序通过设置控制寄存器cr0的标志(PG位31)来启动对内存的分页处理功能,并设置各个页表项* 的内容,以恒等映射前16MB的物理内存。分页器假定不会产生非法的地址映射(也即在只有4MB的机器上* 设置出大于4MB的内存地址)** 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能直接使用>1MB* 的地址。所有"普通"函数仅使用低于1MB的地址空间,或者是使用局部数据空间,该地址空间将被映射到* 其他一些地方去--mm(内存管理程序)会管理这些事的.**/# 上面英文注释第2段的含义是指在机器物理内存中大于1MB的内存空间主要被用于主内存区。主内存区空间# 由mm模块管理,它涉及页面映射操作。内核中所有其它函数就是这里指的"普通"函数。# 初始化页目录表前4项和4个页表
.align 4
setup_paging:movl $1024 * 5, %ecx /* 5 pages - pg_dir+4 page tables */xorl %eax, %eaxxorl %edi, %edi /* pg_dir is at 0x000 */ # 页目录从0x0000地址开始cld;rep;stosl # eax内容存到es:edi所指内存位置处,且edi增4.# 设置页目录表中的前4个页目录项# 例如第1个页目录项:# 页表所在地址 = 0x00001007 & 0xfffff000 = 0x1000# 页表属性标志 = 0x00001007 & 0x00000fff = 0x07 表示该页存在,用户可读写.movl $pg0 + 7, pg_dir /* set present bit/user r/w */movl $pg1 + 7, pg_dir + 4 /* --------- " " --------- */movl $pg2 + 7, pg_dir + 8 /* --------- " " --------- */movl $pg3 + 7, pg_dir + 12 /* --------- " " --------- */# 设置4个页表中所有项的内容(共4096项),从最后一个页表的最后一项开始按倒退顺序填写movl $pg3 + 4092, %edi # edi->最后一页的最后一项.movl $0xfff007, %eax /* 16Mb - 4096 + 7 (r/w user,p) */std # 方向位置位,edi值递减(4字节)
1: stosl /* fill pages backwards - more efficient :-) */subl $0x1000, %eax # 每填好一项,物理地址值减0x1000。jge 1b # 如果小于0则说明全填写好了cld# 设置页目录表基地址寄存器cr3(保存页目录表的物理地址)xorl %eax, %eax /* pg_dir is at 0x0000 */movl %eax, %cr3 /* cr3 - page directory start */# 设置启动使用分页处理(cr0的PG标志,位31)movl %cr0, %eaxorl $0x80000000, %eax # 添上PG标志movl %eax, %cr0 /* set paging (PG) bit */ret /* this also flushes prefetch-queue */# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
# 该返回指令ret的另一个作用并跳转到/init/main.c程序去运行。
head.s程序总结:
- 设置了GDT和IDT ,检查A20地址,重新初始化段寄存器,特别注意设置的堆栈地址。
- 检查x87处理器是否存在设置仿真标志位,复位协处理器存在MP位。
- 压栈main可能使用的参数,设置16M的页表映射,然后跳转到main函数执行。
boot总结:
读取setup到0x90200和system代码到0x10000,设置根设备号到内存0x90000+508跳转到steup运行,setup读取硬件参数到0x90000-0x90200,关闭cpu中断,初始化8259A芯片,设置idt(限制和基地址都设为0)和gdt描述符(1个代码段,1个数据段都是(0-8M字节)),打开A20地址实现1M以上地址的寻址,打开保护模式,跳转到地址0运行head的代码,head代码重新设置了GDT代码限制改为16M,数据段也改为16M都从0-16M地址,一共就2段,idt设置256个一样的中断门都打印一样的数据,设置堆栈为c语言定义的stack_start,4K字节,设置DS等指向数据段,这里重新设置了几次主要是修改了gdt需要刷新段寄存器隐藏的部分缓存,最后设置页表页目录覆盖从0开始的部分代码,实现的映射16M对应物理地址16M没做转换,最后打开分页管理,跳转到main函数执行,这里使用提前压栈的main用的ret指令执行main函数。
现在操作系统已经有了简单的中断处理函数,页管理16M,代码段描述符和数据段描述符都为16M,描述符基地址限制是一样的只是属性不同,段寄存器都指向的数据段描述符,esp指向的stack_start,硬件参数存放到0x90000-0x90000+510,然后返回到压栈的c语言的main函数开始运行。
参考资料:
《Linux内核设计的艺术》【作者:新设计团队】
《Linux内核完全注释》 【作者:赵炯】
《Orange‘s一个操作系统的实现》【作者:于渊】
《x86汇编语言-从实模式到保护模式》【作者:李忠】
https://github.com/1358484518/linux0.11-master
linux0.1x内核代码学习笔记-boot启动相关推荐
- windows内核开发学习笔记十五:IRP结构
windows内核开发学习笔记十五:IRP结构 IRP(I/O Request Package)在windows内核中,有一种系统组件--IRP,即输入输出请求包.当上层应用程序需要访问底层输入输 ...
- LIteOS学习笔记-7LiteOS启动流程与编译流程
LIteOS学习笔记-7LiteOS启动流程与编译流程 LiteOS启动流程 1. 启动方式 2. 启动流程 硬件初始化 内核初始化 调试串口初始化 尝试进行网络连接 启动任务调度 LiteOS编译流 ...
- windows内核开发学习笔记十八:IRP 处理的标准模式
windows内核开发学习笔记十八:IRP 处理的标准模式 在 Windows 内核中的请求基本上是通过 I/O Request Packet 完成的. I/O manager ---> Dis ...
- Contiki学习笔记——Cooja启动失败
Contiki学习笔记--Cooja启动失败 Cooja启动 Cooja启动 进入Terminal: cd Contiki/tools/cooja ant run 出现错误: Could not fi ...
- GEE(Google Earth Engine) 代码学习笔记一 快速入门
GEE 代码学习笔记一 (GEE 基于JavaScript语言和python语言,我记录的是JavaScript语言) 1.GEE 快速入门 quick start. 2.基本语句 - 简单输出 pr ...
- windows内核开发学习笔记十七:IRP 和 IO_STACK_LOCATION 的交互
windows内核开发学习笔记十七:IRP 和 IO_STACK_LOCATION 的交互 前面两篇学习笔记分别介绍了IRP和IO_STACK_LOCATION,整个设备栈来处理这个IRP,但是每个设 ...
- frustum pointnets训练代码学习笔记——kitti_object.py
frustum pointnets训练代码学习笔记--kitti_object.py 本文记录了博主学习frustum pointnets过程中遇到的2D和3D数据库显示程序.为了画出输出结果,博主希 ...
- GEE (Google Earth Engine)最基础代码学习笔记三
GEE (Google Earth Engine)代码学习笔记三 本次学习核心为:将JavaScript objects and primitives放入Earth Engine 容器传到服务器,并处 ...
- DSB2017项目grt123代码学习笔记一:项目基本情况
DSB2017项目grt123代码学习笔记一:项目基本情况 Kaggle上Data Science Bowl 2017年肺结节检测比赛第一名grt123团队的算法. github地址:https:// ...
最新文章
- 能打造新型CPU的有机分子元件登Nature,用if语句攒出决策树,一个顶数千晶体管...
- Ubuntu安装okular PDF阅读器
- 【报告】一手资料:四线城市移动互联网用户调研
- debian下运行netstat失败
- 基于51单片机的计算器
- sql 时态表的意义_在SQL Server 2016中拉伸时态历史记录表
- rrdtool源码安装(转)
- 大数据:HBase安装、配置及使用
- SAP B1 VS女士的Navision ERP的比较 - 知道如何选择软件
- PPT文件太大了怎么压缩
- itunes备份是整个手机备份吗_iTunes备份道理我都懂,但我依然不想备份的?
- 12.分布式定时任务(xxl-job)
- About The Idea Of Flipped
- 【no-descending-specificity】问题
- 计算机微软云同步怎样安装软件,在windows10/8/7系统安装和设置OneDrive 同步文件...
- 看涨期权计算函数实现(Python)
- Python|简易银行ATM程序制作
- mysql数据库名称中包含短横线的对应方式
- virtualbox折腾记
- 鸿蒙符和无级符,《少年三国志》乾坤无极评析:乾坤闪避控制废,无极复活两将兴?...