目录

1. 引入保护模式对程序加载与执行的影响

1.1 对应用程序的影响

1.2 对操作系统的影响

1.3 本章程序总体结构

2. MBR加载内核过程分析

2.1 内核头部段分析

2.1.1 内核总长度

2.1.2 内核段布局

2.1.3 内核入口点

2.2 创建GDT进入保护模式

2.3 读取内核

2.3.1 读取函数分析

2.3.2 读取内核流程分析

2.4 建立内核段描述符

2.4.1 寻址GDT表

2.4.2 建立段描述符(以公共例程段为例)

2.4.3 make_gdt_descriptor过程分析

2.4.4 更新GDT表

2.5 跳转到内核执行

3. 在内核中执行

3.1 显示字符串

3.2 使用cpuid指令获取处理器品牌信息

3.2.1 cpuid指令功能

3.2.2 cpuid指令使用

4. 内核加载应用程序过程分析

4.1 应用程序头部段分析

4.1.1 应用程序总长度与程序头部长度

4.1.2 应用程序栈选择子与长度

4.1.3 应用程序代码段选择子与长度

4.1.4 应用程序入口点

4.1.5 应用程序数据段选择子与长度

4.2 读取应用程序

4.2.1 load_relocate_program过程的参数

4.2.2 读取应用程序首个扇区

4.2.3 计算应用程序所需内存

4.2.4 分配内存

4.2.5 读取整个应用程序

4.3 建立应用程序段描述符

4.3.1 构造段描述符

4.3.2 安装段描述符

4.3.3 回填段选择子

4.4 跳转到应用程序执行

4.5 应用程序返回后

4.5.1 应用程序返回

4.5.2 内核的后续操作


1. 引入保护模式对程序加载与执行的影响

1.1 对应用程序的影响

① 引入保护模式并不会增加应用程序的开发负担,在加载应用程序时,为程序的每个段创建描述符,是操作系统的工作,不需要应用程序操心

② 为了配合操作系统的加载,应用程序需要提供一些必要的信息(一般以头部信息的方式存在),来帮助操作系统将自己加载到内存中

说明:每种操作系统都有自己支持的应用程序格式,比如Linux中的ELF格式。这些格式均定义有头部信息,用以帮助应用程序的加载

1.2 对操作系统的影响

① 操作系统需要考虑以何种方式来加载应用程序,并在适当的时候将处理器的执行流转移到用户代码中

② 为了减轻用户程序的工作量,操作系统还应当管理硬件,并提供大量的例程供应用程序使用

同时,操作系统和应用程序应当协商一种机制,让应用程序能够在使用这些例程时,不必考虑例程的位置

说明:在目前的操作系统中,一般通过系统调用向应用程序提供服务功能

1.3 本章程序总体结构

2. MBR加载内核过程分析

2.1 内核头部段分析

MBR需要根据内核头部段提供的信息,完成对内核的加载,因此我们先分析内核头部段包含的内容

2.1.1 内核总长度

① 内核总长度帮助MBR确定要读取的扇区个数

② 内核总长度的获取依赖core_end标号,该标号所在的段在内核程序最后且不添加vstart = 0子句,因此该标号的汇编地址在数值上就是内核总长度

2.1.2 内核段布局

① MBR在加载内核时,需要为内核中的各个段建立段描述符,而段描述符中需要线性段基址

此处提供了内核中各段的汇编地址,加上加载内核的起始物理地址,就可以计算出各段在0 ~ 4GB线性空间中的线性段基址

② 上述各段在内核中定义时,均添加了vstart=0子句,配合段的线性基地址,使用段中的标号即可实现段内寻址

说明:关于内核段布局

在本章示例程序中,内核程序由如下段组成

① 内核头部段

用于记录内核总长度,每个段的相对位置以及内核入口信息,用于告诉MBR如何加载内核

② 公共例程段

本质上是一个代码段,包括了一些可以反复使用的子过程,包括在屏幕上显示字符串的例程,硬盘读写的例程,内存分配的例程,描述符的创建和安装例程等

这些子过程可以被内核自己使用,也可以供用户程序使用

③ 数据段

包括系统核心数据,这些数据供内核自己使用

④ 代码段

包含进入内核之后首先要执行的代码,以及读取和加载用户程序,控制用户程序执行的代码

2.1.3 内核入口点

① MBR在执行的最后会通过间接绝对远转移跳转到内核入口点执行

② 由于是在保护模式下,内核入口点包含段选择子和段内偏移地址。该段选择子通过equ伪指令定义,对应的段描述符在MBR加载内核的过程中,由MBR建立

2.2 创建GDT进入保护模式

此处创建GDT表并进入保护模式的操作在之前的笔记中已有说明,初始化之后的内存布局如下图所示,

说明:本章程序中,

① MBR会将内核加载到内存线性地址0x00040000(256KB)处

② 内核会将用户程序加载到内存线性地址0x00100000(1MB)处

2.3 读取内核

2.3.1 读取函数分析

本章程序依然将读取一个扇区的功能抽象为一个过程,总体上与之前实现的过程相同,只是需要注意如下2处不同

① 使用EAX寄存器传递逻辑扇区号

之前在实模式阶段,通用寄存器只有16位,因此使用DI:SI存储28位逻辑扇区号。在保护模式下,可以直接使用32位的通用寄存器

② 返回的EBX比入参增加512B,这样后续的读取操作可以继续使用EBX的值进行连续访问。之前在实模式下,受限于64KB的段长度,每读取512B就将DS增加0x20

说明:内核的加载地址与起始逻辑扇区号使用equ伪指令定义

2.3.2 读取内核流程分析

说明:上述过程中,or edx, edx指令用于判断内核总长度是否能被512整除,

① 如果余数不为0,eax中为(总扇区数 - 1),因为已经读取1个扇区,eax中就是要读取的剩余扇区数

② 如果余数为0,eax中为总扇区数,因为已经读取1个扇区,(eax - 1)才是要读取的剩余扇区数

在实际读取剩余扇区之前,还通过or eax, eax指令判断剩余扇区数是否为0,如果为0,则不能用该值进入loop循环(会循环读取0xFFFFFFFF + 1次),而是要跳转到下面建立段描述符的步骤

2.4 建立内核段描述符

2.4.1 寻址GDT表

① 在实模式下,是使用CS:段超越前缀来寻址GDT表。但是,在保护模式下,代码段是否可读由段描述符指定,且代码段是始终不可写的

因此我们通过0 ~ 4GB的数据段描述符来访问GDT表,因为我们不仅要读取,还要通过写入新增GDT表项

② 因为MBR被加载到内存0x7C00地址处运行,因此寻址GDT表的偏移量如下图所示,

2.4.2 建立段描述符(以公共例程段为例)

下面分别说明建立段描述符所需的3个组件如何得到,

① 线性段基址

线性段基址 = 内核加载地址(0x00040000)+ 内核段汇编地址(e.g. 公共例程段汇编地址)

② 段界限

对于向上扩展的段,段界限 = 段长度 - 1,因此问题就转化为获取段的长度。又由于内核中各段有确定的先后顺序,而且是紧挨着的,因此前后2个段起始汇编地址之差,就是段长度

对于公共例程段,段长度 = 内核数据段起始汇编地址 - 公共例程段起始汇编地址

③ 段属性

段属性在过程中直接指定,且与段描述符中的比特位对应

说明:内核被加载到内存后的布局如下图所示,

2.4.3 make_gdt_descriptor过程分析

make_gdt_descriptor过程的执行过程,注释的已经非常清晰,这里主要说明一下bswap指令

bswap(Byte Swap)指令格式如下,在标准的32位处理器上,只允许32位的寄存器操作数

bswap r32

bswap指令的字节交换效果如下,

在创建段描述符的过程中,bswap指令可以达到如下图所示的目的,

2.4.4 更新GDT表

在新增GDT表项之后,更新了GDT表大小,同时使用lgdt指令重新加载GDTR寄存器,从而使得修改生效

说明:MBR更新后的GDT表如下图所示,

2.5 跳转到内核执行

此处通过间接远转移指令,跳转到内核入口点运行。至此,MBR的任务就结束了

3. 在内核中执行

3.1 显示字符串

① 要显示的字符串定义在内核数据段,内核数据段描述符由MBR在加载内核时建立

② 通过直接绝对远跳转,跳转到公共例程段的put_string过程运行,此时会同时保存[CS : EIP]

③ 与远跳转对应,put_string过程需要用retf指令返回,以便同时恢复[CS : EIP]

3.2 使用cpuid指令获取处理器品牌信息

3.2.1 cpuid指令功能

① cpuid(CPU Identification)用于返回处理器的标识和特性信息

② EAX用于指定要返回什么样的信息,也就是功能号,有时还要用到ECX寄存器

③ cpuid指令执行后,处理器将返回的信息存储在EAX / EBX / ECX / EDX中

说明:cpuid指令的使用场景

很多新功能只有新处理器才有,旧处理器并不支持。因此软件可以通过cpuid指令判断当前处理器是否支持特定功能(e.g. 是否支持多媒体扩展指令集),并做出不同处理

3.2.2 cpuid指令使用

以功能号0为例,cpuid指令返回如下信息

说明1:cpuid指令从80486处理器开始引入,可以通过EFLAGS寄存器中的ID位,判断当前的处理器是否支持cpuid指令(0:不支持,1:支持)

说明2:示例中使用的3个功能号只有在奔腾4之后才支持,因此对于cpuid指令比较完备的使用步骤为,

① 通过EFLAGS寄存器的ID位判断当前处理器是否支持cpuid指令

② 如果处理器支持cpuid指令,则先使用功能号0获取最大支持的功能号,之后再判断当前功能号是否支持

4. 内核加载应用程序过程分析

4.1 应用程序头部段分析

内核需要根据应用程序头部段提供的信息,完成对应用程序的加载,因此我们先分析应用程序头部段包含的内容

4.1.1 应用程序总长度与程序头部长度

① 应用程序总长度供内核判断需要读取的扇区个数,获取方式依然是在程序最后定义trail段,并定义program_end标号

② 此处保存应用程序头部长度,是因为内核中将建立应用程序头部段描述符,后续内核在跳转到应用程序入口点时,将基于该头部段进行索引

当然,也可以与MBR一样,基于0 ~ 4GB数据段进行索引

4.1.2 应用程序栈选择子与长度

① 内核在加载应用程序时,将为其创建栈段描述符,stack_seg供内核回填相应的段选择子

② stack_len供应用程序指定栈大小(以4KB为单位),内核在为应用程序创建栈段时,会据此分配内存

4.1.3 应用程序代码段选择子与长度

内核在加载应用程序时,将为其创建代码段描述符

a. 在创建时,内核根据code_seg & code_len获取应用程序代码段的汇编地址与段长度

b. 创建完成后,将段选择子回填到code_seg中

说明:code_seg先由应用程序写入代码段汇编地址,因此定义为双字(32位);而内核回填的段选择子为16位,只使用了低位的2B

4.1.4 应用程序入口点

prgentry的4B与code_seg中的低2B,共同构成了应用程序的入口点

4.1.5 应用程序数据段选择子与长度

与应用程序代码段类似,内核在加载应用程序时,将为其创建数据段描述符

a. 在创建时,内核根据data_seg & code_len获取应用程序数据段的汇编地址与段长度

b. 创建完成后,将段选择子回填到data_seg中

说明1:符号地址检索表的相关内容在下章笔记介绍

说明2:为什么选择段选择子回填而不是固定段选择子 ?

根据上文分析,内核固定了自身各段的段选择子,而应用程序则依靠内核在加载时回填段选择子。这是因为应用程序的加载是由内核确定的,而且系统中会有多个应用程序需要加载,而且可能还需要反复加载,因此不能由应用程序来指定自身的段选择子

这就像应用程序为什么需要有重定位的能力,因为应用程序不能决定自己被加载的内存地址

我们在此处扩展讨论一下有无段寄存器(即段机制)对程序重定位的影响

① 首先,程序(内核 & 应用程序)在编译之后会有汇编地址,如果是多个文件经过链接产生最后的二进制文件,则会有链接地址

汇编地址或链接地址是静态的,是在编译过程中产生的

② 当我们将编译后的二进制文件加载到物理内存时,如果链接地址与加载的内存地址不匹配,就需要进行重定位

a. 如果有段机制,只要将段寄存器指向加载的内存地址,即可以使用程序中的标号,也就是段内偏移进行寻址

b. 如果没有段机制,则需要将程序拷贝到与链接地址匹配的内存处,否则程序中的地址相关操作将运行错误。这里要注意,一旦没有段机制,也就没有了段内偏移的概念,所有地址都是0 ~ 4GB线性地址空间中的一个地址

此时也可以理解为,只有一个段,段线性基地址为0,长度为4GB,所有的链接地址都是基于0地址的偏移

③ 而弥合没有段机制时链接地址与运行地址不匹配情况的,就是分页机制。我们在分页机制的基础上构建了虚拟地址,每个应用程序可以独占4GB的虚拟地址空间,而实际的物理加载位置,由分页机制 + 缺页异常来处理

这样,所有的应用程序都可以用相同的方式进行编译链接,操作系统在加载时,为每个应用程序新建一个4GB的虚拟地址空间

④ 由于很多体系结构的处理器没有分段机制只有分页机制,而分页机制又可以解决程序加载中的重定位问题(甚至使得这不再是一个问题),因此Linux操作系统在X86体系结构中选择绕过分段机制,将逻辑地址和线性地址统一,也就是使用平坦模型

⑤ 在后续的学习中需要注意一点,就是分页模式默认是不开启的,也就是在处理器运行的初始阶段,都是运行在实模式(X86 & ARM)。此时是需要我们处理好程序链接地址与运行地址的关系的,比如不执行地址相关操作,或者将程序拷贝到链接地址处

说明3:在示例程序中,我们自己构建了应用程序头部段。在实际操作系统中,即使是大多数汇编语言,也不需要自己构造头部段,而是由链接器(Linker)完成

4.2 读取应用程序

4.2.1 load_relocate_program过程的参数

可见应用程序需要被烧写到磁盘的第50个逻辑扇区,从这里可以看出文件系统在操作系统中的重要性,他可以使得文件的存储位置不再需要硬编码在程序中

4.2.2 读取应用程序首个扇区

读取扇区的函数依然使用read_hard_disk_0,此处的不同是,我们将应用程序的首个扇区读取到内核数据段开辟的core_buf缓冲区中

这是因为此处我们选择动态为应用程序分配所需内存,而此时尚未分配

4.2.3 计算应用程序所需内存

① 由于磁盘为块设备,只能以扇区为单位进行读取,因此我们计算以512B为单位的应用程序长度(向上取整)

② 这里使用了条件传送指令CMOVxx(Condition MOV),目的是避免使用转移指令,提高流水线效率

说明1:为何避免使用转移指令 ?

在早先的处理器中,转移指令是影响处理器速度的重要因素之一。因为转移指令会清空流水线

后续处理器引入分支预测技术,但是不一定每次都预测成功。所以最好的办法就是避免使用转移指令

说明2:条件传送指令族CMOVxx

条件传送指令从P6处理器开始引入,因此并非所有处理器都支持,可以用功能号1执行cpuid指令,判断返回的EDX中的bit 15,为0不支持,为1支持

条件传送是根据标志位判断的一个指令族,指令格式如下,

cmovxx r, r/m

cmov指令的目的操作数只允许是16 / 32 / 64位寄存器,源操作数可以是相同尺寸的寄存器或内存单元

cmov指令不影响任何标志位

说明3:test eax, 0x000001ff的作用

用于判断向上取整前的程序大小是不是512B对齐的,如果是的话,则使用原大小,否则会多读取一个扇区

4.2.4 分配内存

4.2.4.1 内存分配的基本策略

内存分配需要确定2个问题,

① 第1次分配从哪个地址开始

② 每次分配后,需要确定下一次分配的地址

为了解决上面的问题,在内核数据段中定义了ram_alloc标号,该位置用于存储下次内存分配时的起始地址。而该标号的初始值为0x00100000,也就是第1次分配时的起始地址

每次分配后,都要修改ram_alloc变量的值,用来记录下一次分配内存时的起始地址

4.2.4.2 内存分配的简易实现过程

说明:本章示例中,只分配内存,不回收内存

说明:将内存地址4B对齐,是因为32位处理器访问4B对齐的地址速度最快

4.2.5 读取整个应用程序

构造loop循环,读取应用程序的所有扇区到分配的内存

4.3 建立应用程序段描述符

我们以应用程序头部段为例,说明建立应用程序段描述符的过程

4.3.1 构造段描述符

构造段描述符时,依然是需要段线性基地址、段界限和段属性,构造时使用的过程与MBR中的类似

4.3.2 安装段描述符

安装描述符时使用了set_up_gdt_descriptor过程,为了安装描述符,首先在内核数据段定义了pgdt标号

pgdt标号处共6B(低2B + 高4B),低2B保存GDT表的界限值,高4B保存GDT表的线性地址。只有获取到这2个参数,我们才能修改GDT表,在其中增加新的表项

下面分析set_up_gdt_descriptor过程

说明1:sgdt指令

set_up_gdt_descriptor过程是在GDT表中安装新的描述符,因此需要知道GDT表的地址和大小

当前程序的GDT,是在MBR中创建的,位置和大小信息在MBR中指定。为了获取GDT表的位置和大小信息,可以使用sgdt(Store Global Descriptor Table Register)指令

sgdt m6

sgdt指令将GDTR寄存器的内容存入指定的内存处,这里的m6是指指定的内存处必须有6B的内存空间,用来保存GDT的32位线性地址和16位界限值

sgdt指令不影响任何标志位

说明2:新增表项的偏移量

GDT表的大小,就是新增表项在GDT表中的偏移量。因此我们先获取GDT表界限,将其加1,即得到GDT表大小

新增表项在GDT表中的偏移量 + GDT表线性地址 = 新增表项的线性地址

说明3:movzx指令

获取GDT表界限时,因为目的操作数的宽度(4B)大于源操作数的宽度(2B),使用了movzx指令,即带零扩展的传送指令(MOV with Zero-eXtend)

movzx r16, r8/m8
movzx r32, r8/m8
movzx r64, r8/m8
movzx r32, r16/m16
movzx r64, r16/m16

对应的,还有movsx指令(MOV with Sign-eXtend),即带符号位扩展的传送指令,格式与movzx相同

这里之所以会出现目的操作数与源操作数宽度不同的情况,是因为源操作数为段界限,肯定是16位的;但是后续要计算新增表项的线性地址,可能是32位的,因此使用了ebx。这就造成了操作数宽度不同的情况

说明4:计算最后一个表项的索引

① GDT表项索引是从0开始的

② GDT表大小肯定是8的倍数,GDT表界限 = GDT表大小 - 1,将其除以8正好对应最后一个表项的索引值

说明5:表项索引构成段选择子

将计算得到的表项索引左移3位,则TI = 0,即使用GDT表;RPL = 0b00,正好构成当前的段选择子

说明6:计算新增表项偏移量时为何使用inc bx ?

这里使用inc bx而不是inc ebx是有原因的。如果在调用该过程前还没有建立过段描述符,则GDT表为空。在处理器上电后,默认的GDT表界限为0xFFFF,也就是说,此时ebx中的值为0x0000FFFF

如果使用inc bx,则ebx中的结果为0x00000000,这是正确的;如果使用inc ebx,则ebx中的结果为0x00010000,反而是不正确的

4.3.3 回填段选择子

此处将计算得到的段选择子回填到应用程序头部段中,供应用程序在接管控制权后使用。段选择子为16位,所以回填时使用2B

说明:关于应用程序的栈段

① 在教材中,使用向下扩展的段作为栈段

使用向下扩展的段作为栈段的难点在于计算段线性基址和段界限,

a. 段界限

此处为0x000ffffe,也就是占用1个粒度,即4KB(32位向下扩展的段,段内最大偏移为0xfffff)

b. 段线性基地址

乘法运算后eax为栈段大小,加上栈段分配到的内存起始地址,就是栈的线性基地址

② 在课程中,使用向上扩展的段作为栈段,而且是由应用程序直接指定(与定义应用程序数据段类似)

在实际操作系统中,也是使用向上扩展的段作为栈段(向下扩展的段用起来太麻烦了)

4.4 跳转到应用程序执行

在课程中,使用如下方式跳转到应用程序执行

mov [esp_pointer], esp ;保存内核栈指针
mov ds, ax      ;ax为应用程序头部段选择子
call far [0x08] ; 跳转到应用程序入口点

说明1:保存内核栈指针

我们已经为应用程序分配了栈段,跳转到应用程序后,将使用应用程序的栈。此处保存内核栈指针,等后续从用户程序返回,可以继续使用

说明2:跳转到应用程序方式

在教材中,使用如下方式实现跳转

jmp far [0x10]

与其对应的应用程序返回方式将在下章笔记中介绍

4.5 应用程序返回后

4.5.1 应用程序返回

在课程使用的代码中,应用程序通过retf指令返回

这里使用retf指令返回是有一个前提条件的,就是在应用程序中不能切换栈(SS寄存器),因为内核是将返回地址(CS : EIP)保存在内核栈的

4.5.2 内核的后续操作

用户程序使用retf返回后,内核恢复段寄存器,指向内核自己的数据段和栈段,恢复到进入用户程序之前的状态

X86汇编语言从实模式到保护模式13:保护模式程序的动态加载和执行相关推荐

  1. ASM:《X86汇编语言-从实模式到保护模式》第13章:保护模式下内核的加载,程序的动态加载和执行...

    ★PART1:32位保护模式下内核简易模型 1. 内核的结构,功能和加载 每个内核的主引导程序都会有所不同,因为内核都会有不同的结构.有时候主引导程序的一些段和内核段是可以共用的(事实上加载完内核以后 ...

  2. 李忠 X86汇编语言 从实模式到保护模式-初学

    学习资料: 教学视频 网易云课堂 哔哩哔哩 原书网站 原书相关源码附件下载 网友帖子 除了后面没有图片之外很不错的笔记总结,写者很用心 留存待看,一片文章写了特点 很有特色总结的笔记 学习目标: 15 ...

  3. x86汇编语言从实模式百度云_x86汇编语言:从实模式到保护模式

    x86汇编语言:从实模式到保护模式2013年1月由电子工业出版社出版发行,总共6000行的源代码,全方位地向读者展现汇编语言程序设计之美.尽管汇编语言也是一种计算机语言,但却是与众不同的,与它的同类们 ...

  4. Android动态加载进阶 代理Activity模式

    基本信息 作者:kaedea 项目:android-dynamical-loading 技术背景 简单模式中,使用ClassLoader加载外部的Dex或Apk文件,可以加载一些本地APP不存在的类, ...

  5. x86汇编语言从实模式百度云_Intel x86 CPU 32位保护模式杂谈之任务切换 上

    目录: 什么是任务 任务由什么组成 任务门描述符是什么东东?有了TSS描述符为什么要有任务门描述符? 参考文献 什么是任务 任务(task)是处理器可以分配.执行.挂起的工作单位,笔者认为和我们操作系 ...

  6. 程序的加载和执行(六)——《x86汇编语言:从实模式到保护模式》读书笔记26

    程序的加载和执行(六)--<x86汇编语言:从实模式到保护模式>读书笔记26 通过本文能学到什么? NASM的条件汇编 用NASM编译的时候,通过命令行选项定义宏 Makefile的条件语 ...

  7. 程序的加载和执行(五)——《x86汇编语言:从实模式到保护模式》读书笔记25

    程序的加载和执行(五)--<x86汇编语言:从实模式到保护模式>读书笔记25 前面几篇博文终于把代码分析完了.这篇就来说说代码的编译.运行和调试. 1.代码的编译及写入镜像文件 之前我们都 ...

  8. 程序的加载和执行(四)——《x86汇编语言:从实模式到保护模式》读书笔记24

    程序的加载和执行(四)--<x86汇编语言:从实模式到保护模式>读书笔记24 通过本文能学到什么? 怎样跳转到用户程序 用户程序通过调用内核过程完成自己的功能 怎样从用户程序返回到内核 接 ...

  9. 程序的加载和执行(一)——《x86汇编语言:从实模式到保护模式》读书笔记21

    程序的加载和执行(一) 本文及之后的几篇博文是原书第13章的学习笔记. 本章主要是学习一个例子,对应的代码分为3个文件: ;代码清单13-1;文件名:c13_mbr.asm;文件说明:硬盘主引导扇区代 ...

最新文章

  1. HDU 1429 胜利大逃亡(续)
  2. CCNA必会知识点:PAP单双向认证
  3. 计算机二级mysql模拟_2017年计算机二级MySQL考前模拟练习
  4. leetcode 229. Majority Element II | 229. 求众数 II(找出现次数超过n/k的元素)
  5. python读取csv文件_python3.0读取csv文件
  6. 日常生活开支记账明细_深圳公司如何记账报税代理记账报税的流程以及所需的资料...
  7. php 集字抽奖,php字符集转换
  8. 生产环境实施 VMware 虚拟化基础架构,千万不要犯 4 个错误
  9. 怎样让手机立马变空号?
  10. learning java AWT 布局管理器CardLayout
  11. 使用Ntdsutil.exe捕获系统状态数据
  12. Cadence orcad 使用MySQL搭建元件数据库及实例数据库下载
  13. verilog实现格雷码与二进制码的互换
  14. Python 实验三 使用 TCP 实现智能聊天机器人
  15. 在MAC终端下打开Finder
  16. php中文数组,php数组的定义
  17. 计算机应用媒体,计算机应用技术与计算机多媒体技术哪个好
  18. SaaS企业如何构建与自身增长目标相匹配的市场力?
  19. python汇率兑换_美元与人民币汇率 Python
  20. 低轨卫星传播特性仿真与分析

热门文章

  1. SpringCloudGateway(一) 概览
  2. 阿里云仓库使用小技巧
  3. 创建前缀一样的文件_Win10更快速创建或重命名仅扩展名文件
  4. java随机生成车牌_JDBC:随机生成车牌号,批量插入数据库
  5. java中的getnumber怎么用_java安全编码指南之:Number操作
  6. Python中判断字符串中是否包含另一个字符串
  7. 在html页面中建立文字连接,html中如何建立超链接
  8. 内存泄漏的原因及解决办法_内存泄漏的场景和解决办法
  9. 核心网upf作用_核心网“入门级”科普,你看了没?
  10. 计算机nit证书怎么学,计算机等级考试证书和NIT可以抵免自考中哪些课程?