简介

本文根据慕课资料进行粗略学习操作系统的知识,选择性地写一下lab练习
ucore课程文档
课程地址
其他大佬的lab答案地址
推荐博客1
推荐博客2
建议先阅读《编码:隐匿在计算机软硬件背后的语言》和《X86汇编语言-从实模式到保护模式》

第一条指令

CPU加电后会进行初始化,然后在内存读第一条指令。内存有一部分是ROM、一部分是RAM。断电后RAM信息会消失,但是ROM内容一直都在。
读的第一个指令是CS:IP指向的地址(值应该是默认的),刚加电的CPU处于16位实模式下,寻址空间大小为2的20次方(1MB),CS、IP都是16位的。CS*16+IP(0xFFFF0)是第一条指令的地址,同时CS:IP要在2的20次方的寻址空间内。第一条指令在最底下的1MB空间内。这第一条指令及其跟着的指令就是BIOS,它要提供一些服务,然后CPU才能访问磁盘设备。

BIOS

BIOS从磁盘读引导扇区(512字节)里的加载程序,将其写到内存0x7c00,然后CS:IP跳到那里去执行加载程序(bootloader)。
详细一点说,BIOS只能在实模式下运行,它先是检查硬件,进行设备初始化。修电脑时,显示器不工作,可以猜测内存有问题。因为BIOS先检查到内存有问题,后面它就不用检查了,也不启动系统了。然后检查插入的U盘、磁盘什么的。检查产生的信息成为BIOS数据。虽然ROM数据不会消失,但是因为每次插入的硬件不一样,所以BIOS会改这些数据。最后读我们磁盘的第一个扇区。BIOS先读磁盘的主引导扇区(512字节)。因为这个东西提供的信息可以帮我们选择启动磁盘里的哪个操作系统。根据主引导扇区的信息来选择并读取活动分区(分区引导扇区)。先执行分区引导扇区的跳转指令,跳转到启动代码启动到加载程序。

加载程序

加载程序把磁盘的ucore操作系统数据和代码加载到内存,再跳到ucore起始地址。把控制权交给操作系统。
详细地说,加载程序会先从文件系统中读取启动配置信息,依据这些信息决定怎么加载内核,这个地方如果我们可以弄一个选项在显示屏(启动菜单),可以改参数就很好。最后跳到内核。

ucore结构简单,应该是直接到加载程序。

不够详细

知道这些还是很粗糙,如果写实际程序,还要根据CPU手册、BIOS规范(从哪里读第一条,上文是0xFFFF0)。所以自己写操作系统还是要查很多资料的。

系统启动规范

BIOS

这里的主引导扇区的硬盘分区表只有四个分区信息,每个分区信息16字节(BIOS-MBR)。BIOS-GPT则支持超过四个分区。PXE是网络启动,从服务器下载资料到磁盘来启动,此时BIOS要有网络下载功能,BIOS变复杂了。前面说的是在本地磁盘启动。

UEFI

在所有平台一致地启动操作系统。启动磁盘的任何系统。为了安全,检查引导记录是否可信。只读取满足签名的引导记录。

查资料

知乎BIOS与UEFI区别

加载程序干什么

加载程序(bootloader)先定义全局描述符表,然后让CPU从实模式进入保护模式,最后加载内核文件。

定义全局描述符表

我们知道,为了让程序在内存中能自由浮动而又不影响它的正常执行,处理器将内存划分成逻辑上的段,并在指令中使用段内偏移地址。在保护模式下,对内存的访问仍然使用段地址和偏移地址,但是,在每个段能够访问之前,必须先进行登记。
《X86汇编语言-从实模式到保护模式》

每个段由8字节的段描述符描述,描述表存放描述符。
最主要的描述表是全局描述表(Global Descriptor Table,GDT)。还有一个是局部描述表。
CPU有一个48位全局描述表寄存器(GDTR)。它分为32位的线性地址和16位的边界。GDTR 的线性地址部分保存的是全局描述符表在内存中的起始线性地址。边界保存的是全局描述表的边界,其在数值上等于表的大小(总字节数)减一。

GDT最大大小是16位(216字节),由于在实模式下只能访问1MB 的内存,所以GDT 通常都定义在1MB 以下的内存范围中。

加载程序源代码分析

bootasm.S

#include <asm.h>.set PROT_MODE_CSEG,        0x8                     # 代码段
.set PROT_MODE_DSEG,        0x10                    # 数据段
.set CR0_PE_ON,             0x1                     # 保护模式标志# BIOS根据引导扇区加载bootloader到内存 然后从0x7c00开始执行
.globl start                                        # .globl表示整个程序入口
start:
.code16                                             # 以16位(实模式)执行cli                                             # 禁止中断发生cld                                             # 使方向标志位为0 程序往地址增加方向执行xorw %ax, %ax                                   # ax清零movw %ax, %ds                                   # ds清零movw %ax, %es                                   # es清零movw %ax, %ss                                   # ss清零# 看官方文档 https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_appendix_a20.html# 再看 《X86汇编语言 从实模式到保护模式》 11.5 关于第21条地址线A20的问题# 为了进入32位保护模式 必须先开启A20 大概知道一下 我感觉源代码也没有完全按文档来seta20.1:inb $0x64, %al                                  # 读64h端口获得StatusRegistertestb $0x2, %al                                 # 如果StatusRegister从低到高第2位为0 # 说明无输入 可以进入保护模式jnz seta20.1                                    # 如果al低2位为0 则ZF=0 则不跳转 说明低2位为0movb $0xd1, %al                                 # 向64h发送0d1h命令outb %al, $0x64seta20.2:inb $0x64, %altestb $0x2, %aljnz seta20.2movb $0xdf, %aloutb %al, $0x60# 设置GDTlgdt gdtdesc# 修改CRO寄存器中的保护模式允许位 进入保护模式movl %cr0, %eaxorl $CR0_PE_ON, %eax                            # 与0x1进行或运算movl %eax, %cr0                                 # cr0设置为1ljmp $PROT_MODE_CSEG, $protcseg                 # 以PROT_MODE_CSEG为段地址,protcseg为段内偏移地址.code32                                             # 以32位(保护模式)执行
protcseg:# 设置数据段movw $PROT_MODE_DSEG, %axmovw %ax, %dsmovw %ax, %esmovw %ax, %fsmovw %ax, %gsmovw %ax, %ss# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)movl $0x0, %ebpmovl $start, %espcall bootmain                                   # 调用bootmain# 如果bootmain返回了 继续循环
spin:jmp spin# Bootstrap GDT
.p2align 2                                          # 4比特对齐 不知道啥意思# 32位的处理器具有32根地址线 可以访问的地址范围是0x00000000到0xffffffff# 所以 base为0x0 lim为0xffffffff
gdt:SEG_NULLASM                                     # 空段 8字节SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # 代码段 8字节 type为可读可执行段SEG_ASM(STA_W, 0x0, 0xffffffff)                 # 数据段 8字节 type为只写段# GDT创建
gdtdesc:.word 0x17                                      # 边界(表的总字节数减一)# 3*8-1=23=0x17字节 用一个字的空间储存.long gdt                                       # 线性基地址(GDT首地址)

asm.h里面是实现如何创建GDT,不清楚原理

lab1

练习2:使用qemu执行并调试lab1中的软件

下载上面的lab答案,进入lab1_result目录,执行

make lab1-mon

会出现下面三个窗口

项目自动帮我们在0x7c00打了断点
我的gdb加了pwndbg插件,可以看见这个0x7c00和后面的指令

一直输入命令n进行调试

当执行完call kern_init时qemu屏幕有新的输出

这个过程是先执行bootasm.S
bootasm.S会调用bootmain.c中的bootmain()
bootmain()会调用kern/init/init.c中的kern_init()
大佬的答案中对源码有注释

练习5:实现函数调用堆栈跟踪函数 (需要编程)

我们要实现kern/debug/kdebug.c里面的print_stackframe()
堆栈跟踪函数就是把寄存器信息打印一下

由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释
《ucore_os_docs》

那就很方便了

地址空间&地址生成

概念

物理地址就是总线看见的地址,逻辑地址是进程看到的地址

逻辑地址生成

程序源代码经过编译和汇编后得到预备的地址,再链接添加函数库地址,当加载到内存时会重定向地址。相当于加载之前是确定内部各个指令的相对位置,加载后再确认绝对位置。
上面是加载时生成,还有编译时生成、执行时生成

逻辑地址处理

CPU如果见到一个指令的地址,CPU里面的MMU就把它翻译成物理地址,CPU就找这个地址并结合处理信号来处理。如果逻辑地址访问非法,产生异常。

连续内存分配

为了方便,内存分配大小设计为2的整数次幂

内存碎片

不能利用的内存空间(太小了)

外部碎片和内部碎片

动态内存分配

根据内存占用情况进行分配。方法有最先匹配、最佳匹配、最差匹配
空闲分区列表储存空闲分区
释放分区时合并临近地址的空闲内存分区,调整空闲分区列表

最先匹配

思路:需要分配内存时按从低到高顺序寻找第一个可以满足的空闲内存空间,空闲分区列表按地址顺序排序
优点:简单、在高地址空间有大块的空闲分区
缺点:外碎片多,导致寻找大块时要遍历更多次,寻找大块时较慢

最佳匹配

思路:寻找满足需求中最小的内存分区,空闲分区列表按大小顺序排序(可以由小到大,都行)
优点:大部分分配的尺寸较小时效果好、避免大的空闲分区被拆分、可减小外部碎片大小、相对简单
缺点:内外部碎片更小,更加不能利用

最差匹配

思路:寻找满足需求中最大的内存分区,空闲分区列表按大小顺序排序(可以由大到小,都行)
优点:中等大小的分配较多时,效果最好,小碎片少
缺点:后续大内存分配难

碎片整理

下面是碎片整理的一些方法

紧凑

把进程占用内存压到一起,不过需要指令内容的地址要变,也就是应用程序可以动态重定向。这个操作要在进程处于等待时进行。

分区对换

进程处于等待时把它的数据放到外存

伙伴(伴侣)系统

假设可分配分区大小为2的u次幂,需要分配内存空间为m。如果两倍m大于2的u次幂且m小于等于2的u次幂就分配,否则把2的u次幂分一半,继续跟m比。
合并时注意大小是2的整数次幂。

合并条件

大小相同(2的i次幂)、地址相邻、起始地址较小的块的起始地址必须是2的i+1次幂的倍数

如上图,B应该和C合并,不能和A合并。A起始地址可以把前面内存空间大小加起来得到,是256K(2的8次幂)。要求合并空间的起始地址必须是2的9次幂,所以不行。而B起始地址是2的9次幂,所以B和C合并。看上面的树状图,形象地说就是,合并地址要有相同根节点。其实这里可以用二叉树数据结构。

非连续内存分配

如果最大的连续内存都不够,可以继续非连续内存分配。段式分配的单位内存大,页式小。可以把段式和页式结合起来成为段页式。

非连续内存分配设计目标

提高内存利用效率和管理灵活性
允许一个程序的使用非连续的物理地址空间
允许共享代码与数据
支持动态加载和动态链接

段式储存管理

概念

段表示访问方式和存储数据等属性相同的一段地址空间
进程的段空间由多个段组成:主代码段、子模块代码、公用库代码段、堆栈段(stack)、堆数据(heap)、初始化数据段、符号表等

段访问及实现

段号(s)加段内偏移(addr)
CPU访问一个逻辑地址(逻辑地址由段号和偏移组成)时,用段号查进程的段表中的段描述符(基本内容是基址、长度)。段表由操作系统控制。MMU把长度和偏移做比较,检查越界。如果越界,产生异常。

页式储存管理

概念

物理地址空间基本单位叫页帧(Frame),大小为2的n次方。虚拟地址空间基本单位叫页面(Page)

页帧

物理地址表示:帧号(f)和偏移量(o)
物理地址=f∗2S*2^S∗2S+o

如上图,F=7,S=9,f=3,o=3

页面

逻辑空间地址被划分为大小相等的页
页内偏移等于帧内偏移
通常页号大小不等于帧号,因为页号经过转换后不一定对应相等的帧号
虚拟地址表示:页号(p)和偏移量(o)
虚拟地址=p∗2S*2^S∗2S+o

通过页找到帧

CPU得到逻辑地址,用p到页表找对应的f,两者偏移一样。直接得到帧偏移量

页表

每个进程都有一个页表,每个页面对应一个页表项。页表随进程运行状态动态变化。页表基址寄存器(PTBR)可以告诉我们页表基址。PTBR的值是通过CR3寄存器获取的。

页表项组成

页表由帧号、页表项标志组成
页表项标志:存在位、修改位、引用位
存在位:一个逻辑页号是否存在一个物理帧与它相对应,如果有则为1
修改位:页面内容是否修改
引用位:页面是否有过被引用、被访问

页式储存管理优化

缓存:把一次获取的页表项的后面几项缓存起来,可能下次用
间接访问:多级页表,把一个页表分多个,方便找

快表(TLB)和多级页表

快表(TLB):把近期访问过的页表缓存到CPU里面
TLB由关联储存器实现,因为在CPU里面,所以快
多级页表:见下图,第一级页表基址在PTBR里储存

反置页表

如果页表级数过多,访问页表次数增加,很繁琐,所以有了反置页表。
反置页表是页表与物理地址相对应,所有进程共同使用一张页表

页寄存器

每个帧与一个页寄存器关联,寄存器内容包括:使用位、占用页号(逻辑页号p)、保护位(访问方式,比如:可读,可写)
优点:页表大小相对于物理内存很小、页表大小与逻辑地址空间大小无关
缺点:页表信息对调后,需要依据帧号可找页号、在页寄存器中搜索逻辑地址中的页号困难

页寄存器中的地址转换

对逻辑地址的p和进程ID(PID)的数字之和进行哈希算法,用哈希表映射,减少搜索访问,解决哈希冲突。检查页号和页表的PID跟请求的页号和页表的PID是否一样,其他步骤跟前面一样

反置页表的哈希冲突

用得到的哈希值H(即页表第H条)在页表查时发现对应的PID和页号跟哈希前不一样,没关系,页表还提供下一个PID和页号之和哈希值为H的地址

段式存储管理基础

逻辑地址由s(段号)、p(页号)、o(偏移)组成
物理地址由f(帧号)、o(偏移)
先根据s找段表的对应段表项,再根据p找页表中对应的页表项,最后根据o得到具体物理地址。
通过指向相同的页表基址,可以实现进程间的段共享

实验二 物理内存管理

了解x86保护模式中的特权级

特权级范围是从0到3级
ucore和Linux都只有ring0级(内核级)和ring3级(用户级)

段选择子

段选择子位于段寄存器里面

上图的RPL(描述特权级大小)与数据段相关,后面讲的的CPL与代码段相关,与段描述符的DPL(描述段特权级是ring0还是其他什么)比较,RPL象征的权限大于大于DPL才能执行、访问或者中断。中断门(Interrupt Gate)、陷入门(Trap Gate)也有DPL。
段寄存器DS,ES,FS,GS里面有RPL
段寄存器CS,SS里面有CPL
访问门时:CPL<=门的DPL(也就是门特权级低)&CPL>=段的DPL
为什么CPL>=段的DPL?这就体现了ring3应用程序访问ring0级服务的情况
访问段时:MAX(CPL,RPL)<=段的DPL

了解特权级切换过程

通过中断切换特权级
产生中断时,把当前状态保存在属于ring0的栈里面

ring0到ring3的切换

把属于ring0的栈里面的CS的CPL改为3(ring3),SS的RPL改为3,EIP看情况改

ring3到ring0的切换

把属于ring0的栈里面的CS的CPL改为0(ring0),CS指向地址也改,SS和ESP清除,不要ring3的栈了,EIP改

任务状态段(Task-State Segment)

它保存了寄存器的特权信息,ring3到ring0的切换需要的新栈(SS、ESP在栈里面的信息已经清除)相关的寄存器设置就是根据TSS来的
TSS描述符(保存TSS地址等信息)也放在全局描述表里面
对于ucore,就利用TSS里面的SS、ESP信息
Task Register寄存器会缓存TSS地址信息,方便查找

了解段页表

段寄存器

段寄存器一部分内容是index,作为一个索引来找到全局描述符表中的一个项(段描述符)
如果没有开启页基址,CS:IP等就是物理地址,但还是要通过段描述符来映射

段选择子中的隐藏部

GDT因为占空间大所以放在内存里面,硬件会把GDT的段描述符的关键信息放在段寄存器的隐藏部分(用来缓存)

映射

虚拟地址比线性地址(逻辑地址到物理地址变换之间的中间层)大0xC0000000(7个0)
选择页机制为主

了解UCORE建立段/页表

线性地址由32位组成,0到11位是offset,12到21是table,22到31是directory(页目录)。根据directory从page directory(页目录表)找到对应的项PDE,比如directory就表示查表的第directory项,后面也是一样。PDE记录的是二级页表里面的起始地址。根据table从page table找到对应的项PTE(page table entry),PTE存的是线性地址对应的页的起始地址。PTE左移三位加上offset作为最终物理地址。
CR3寄存器保存页目录表的起始地址。

页表或者页目录表包含哪些信息

除了关注页表或者页目录表中的基址信息,还有关注它们的属性。
比如是否可读,是否ring3级别能访问等。

使能页机制

CR0第0位(PE)如果置1,则enable保护模式,第31位(PG)如果置1,就代表使能了页机制

虚拟存储概念

存储器速度排行

虚拟储存需求

内存空间不够时,有两种方法
覆盖:应用程序手动把需要的指令和数据分时间段在内存中保存和清除
交换:操作系统自动把暂时不能执行的程序保存到外存中
虚拟储存:在有限容量的内存中,以页为单位自动装入更多更大的程序

覆盖技术

把程序分为必要部分和不必要部分,必要部分在内存中常驻,不必要部分共用同一段内存,相互覆盖

A为必要部分,其他为不必要部分
缺点:需要划分功能模块,并确定模块间的覆盖关系,增加编程复杂度,时间换空间

交换技术

以进程为单位,将暂时不能运行的程序放到外存,程序在换入时要重定位
交换时机:只有当内存空间不够或者可能不够时换出

局部性原理

时间局部性:一条指令两次执行或者一个数据的两次访问都集中在一个较短时期内
空间局部性:邻近的指令和数据集中在一个较小区域内
分支局部性:一条跳转指令的两次执行,很可能跳到相同的内存位置,比如循环,重复跳到循环第一句

虚拟存储的基本概念

思路:将不常用的部分内存块暂存到外存
原理:装载程序时,只将当前指令执行需要的部分页面或段装在内存内。如果指令执行中需要的指令或数据不在内存(缺页或者缺段),处理器通知操作系统将对应的页面或段调入内存。操作系统将内存中暂时不用的页面或段保存到外存。
实现方式:虚拟页式存储、虚拟段式存储
特征:不连续性,物理内存分配非连续,虚拟地址空间使用非连续。大用户空间,提供给用户的虚拟内存可大于实际的物理内存。部分交换,虚拟存储只对部分虚拟地址空间进行交换

虚拟页式存储地址转换

地址转换和页式存储的地址转换一样

页表项结构

下面是一级页表,参考一下,其他页表内容差不多

驻留位:表示该页是否在内存,1表示在,0表示在改页外存,访问该页时将导致缺页异常
修改位:回收该物理页面时,据此判断是否要把它的内容写回外存
访问位:表示该页面是否被访问过(读或写)
保护位:表示该页的允许访问方式(只读、可读写、可执行等)

缺页异常

CPU读取一条指令,会去找这条指令所对应的页表项,如果这一项是无效的,会发生缺页异常,缺页异常服务例程执行。

缺页异常服务例程在外存中找对应那一页在哪里,读入到内存的空闲地方(空闲页帧),然后修改页表项,最后重新执行这条指令。如果没有空闲页帧,把不常用的写出去。它们在物理内存里,要先根据物理页帧f找到逻辑页q,如果逻辑页q被修改过,则把它写回外存,修改q页表项驻留位为0,将需要访问的页p装入到物理页面f,修改p页表项驻留位为1,物理帧号为f。重新执行产生缺页的指令。实际ucore的源代码实现起来会复杂很多。

ucore概述(操作系统学习)相关推荐

  1. c语言 字母 八进制表示'/1011',C语言C语言第一课:C语言概述为什么学习C语言怎样学习C语言.DOC...

    [摘要]C语言 第一课: C语言概述 为什么学习C语言 怎样学习C语言 参考资料 ----------------------------------------------------------- ...

  2. 嵌入式Linux操作系统学习规划,学习嵌入式开发需要哪些知识?

    嵌入式Linux操作系统学习规划 ARM+LINUX路线,主攻嵌入式Linux操作系统及其上应用软件开发目标: (1) 掌握主流嵌入式微处理器的结构与原理(初步定为arm9) (2) 必须掌握一个嵌入 ...

  3. Linux操作系统学习笔记【入门必备】

    Linux操作系统学习笔记[入门必备] 文章目录 Linux操作系统学习笔记[入门必备] 1.Linux入门 2.Linux目录结构 3.远程登录 3.1 远程登录Linux-Xshell5 3.2 ...

  4. 操作系统学习(八)进程同步与通信

    目录 学习建议: 基本内容: 一.概述: 二.进程的顺序性: 三.进程的并发性: 四.与时间有关的错误: 五.临界区的概念: 六.进程的互斥: (一)PV操作: (二)临界区的管理: (三)用PV操作 ...

  5. linux操作系统学习网站整理(100个)

    linux操作系统学习网站整理(100个) 评选出的这100个优秀站点,将按照下述20个类别作以评介: (一) 文件下载 (二) 幽默娱乐 (三) 相关新闻 (四) 通用硬体 (五) 专用硬体 (六) ...

  6. 操作系统学习笔记-2.1.5线程概念和多线程模型

    操作系统学习笔记-2019 王道考研 操作系统-2.1.5线程概念和多线程模型 文章目录 5线程概念和多线程模型 5.1知识概览 5.2 什么是线程?为什么要引入线程? 5.3引入线程及之后,有什么变 ...

  7. 操作系统学习笔记-2.1.4进程通信

    操作系统学习笔记-2019 王道考研 操作系统-2.1.4进程通信 文章目录 4进程通信 4.1知识总览 4.2前置知识:什么是进程通信? 4.3共享存储 4.4 管道通信 4.5消息传递 4.6小结 ...

  8. 操作系统学习笔记-2.1.3进程控制

    操作系统学习笔记-2019 王道考研 操作系统-2.1.3进程控制 文章目录 3.进程控制 3.1知识概览 3.2 基本概念 3.2.1什么是进程控制? 3.2.2如何实现进程控制? 3.3进程控制相 ...

  9. 操作系统学习笔记-2.1. 2进程的状态与转换

    操作系统学习笔记-2019 王道考研 操作系统-2.1. 2进程的状态与转换 文章目录 2进程的状态与转换 2.1知识概览 2.2进程的状态-三种基本状态 2.3进程的状态-另外两种状态 2.4进程状 ...

  10. 操作系统学习笔记-2.1.1.进程的定义、组成、组织方式、特征

    操作系统学习笔记-2019 王道考研 操作系统-2.1.1.进程的定义.组成.组织方式.特征 文章目录 2.1.1.进程的定义.组成.组织方式.特征 1.1知识概览 1.2进程的定义 1.3进程的组成 ...

最新文章

  1. 学习vulkan的几个有用的网址
  2. 学python的好处-python语言的优点和缺点
  3. java geom_java.awt.geom 类 Area - Java 中文参考手册
  4. lucky number
  5. 有关fwrite语句的用法
  6. 前端学习(2367):两种方式导航跳转和传参
  7. 复杂推理模型从服务器移植到Web浏览器的理论和实战
  8. 标准exception类层次图
  9. python 写txt 换行_写入txt文本的内容为什么没换行效果?
  10. 查看程序用运时占用的内存
  11. c++之find()函数
  12. Excel打印针式打印机备货单
  13. matlab峰度和偏度,峰度和偏度
  14. 该如何彻底删除电脑上的软件卸载残留文件?
  15. 同时开发两款H5的ARPG游戏的设计和实践
  16. 软件开发中常见知识总结
  17. 【无标题】汇编实验-学生成绩管理系统
  18. Android软件开发实例:用客户端写博客
  19. 控制台线、console线做法
  20. 基于Sentence-Bert的检索式问答系统

热门文章

  1. 高中会考计算机怎么考,高中会考怎样算通过
  2. 疯狂java讲义(第三版-李刚) 源代码 光盘
  3. 记录一下Java访问https服务出现的异常情况
  4. 【reverse】逆向5 标志寄存器
  5. 关于QQ浏览器jquery获取页面iframe,并调用iframe内方法的问题
  6. 如何使用HTML插槽自动生成目录
  7. 安卓开发学习日记第五天——奇怪的bug出现了(VT-x说没就没)_莫韵乐的欢乐日记
  8. 【笔记】Logstash环境搭建和安装配置
  9. 选择公理_编码者公理
  10. 店铺提升流量有什么作用?用软件提升流量安全吗?