使用Rust开发操作系统(UEFI基本介绍)
UEFI基本介绍
- 关于UEFI
- BIOS
- UEFI介绍
- 引导管理
- UEFI Image
- UEFI 应用程序
- OS Loader
- UEFI运行时服务
- 调用约定
- 调用约定的数据类型
- IA-32架构调用约定
- Rust中的UEFI
- UEFI入口
- OVMF固件制作
- Protocol
- UEFI启动系统过程
- 下一步要做什么
在上一篇文章中我们编写一个基本的操作系统,但是这个操作系统只有很简单的字符输入和输出功能,没有调度,没有内存管理等,但是没关系我们会一一实现他们,现在我们需要解决系统引导启动问题,之前的章节中我们接住了``的Bootloader
库来完成系统引导,但是BootLoader
库只提供了最基本的功能,并且是BOIS
引导启动,为了让我们的系统更具现代化一些,我们使用UEFI
引导启动系统
关于UEFI
在本章中我们只讲述UEFI
的基本介绍,基本的组件,以及加载系统的步骤等,着重讲述使用Rust
来完成UEFI
的编程,有关UEFI
架构的内容可以参考老狼的文章
BIOS
我们要了解UEFI为何物之前需要了解一下BIOS的内容,BIOS全称为Base Input Output System
(基本输入/输出系统),它一组存储在主板ROM中的程序代码主要功能有
- 自检程序,用于开机时对硬件的检测
- 系统初始化,包括对硬件,BIOS中断向量等初始化
- 基本的IO处理
- CMOS设置程序
BIOS运行在16位模式下,实模式下最大可寻址范围为1MB其中0x0C0000~0x0FFFFF留给BIOS使用
启动过程
当按下电源按钮后CPU跳转到0xFFFF0处执行(一般为跳转指令),跳转到BIOS执行入口后BIOS进行加电自检(Power of self Test P.O.S.T.)在自检过程中如果发现硬件错误则通过蜂鸣器报警,P.O.S.T.检测通过后进行检测硬件并将硬件设备初始化,最后根据启动顺序从设备启动,将引导记录通过BIOS中断读入内存BootLoader
(执行的地址从0x7c00开始)
BIOS的缺点
- 开发效率较低(根据程序员):大部分BIOS代码使用汇编开发并且代码与设备的耦合性太高
- 功能扩展性差:增加硬件功能时必须将16位代码放置在0x0C0000 ~ 0x0DFFF区间,并且设置中断处理程序
- 性能差: BIOS的IO操作需要通过中断来完成,并且BIOS没有提供异步工作模式
- 安全性:BOIS运行过程中对代码安全性没有考虑
- 不支持2TB以上的地址引导:BIOS采取32位地址,因而引导扇区的最大逻辑块地址为2^32(2^32 x512=2TB)
UEFI介绍
UEFI(Unified Extensible Firmware Interface)中文统一可扩展固件接口,UEFI主要定义了操作系统和平台固件之间的接口,UEFI只是一种标准,具体的实现由其他公司或开源组成提供
UEFI实现一般分为两个部分
- 平台初始化
- 固件 操作系统接口
UEFI启动过程
UEFI系统从加电到关机分为7个阶段
- SEC(安全验证)
- PEI(EIF前期初始化)
- DXE(驱动执行环境)
- BDS(启动设备选择)
- TSL(操作系统前期加载)
- RT(运行时)
- AL(系统灾难恢复期)
- SEC(Security Phase): 平台初始化的第一个阶段,计算机系统加电后进入这个阶段,他将主要完成接受并初始化系统启动和启动信号,并初始化临时存储区域,在SEC阶段作为可信任系统的根,随后将系统的参数传给下一个阶段,SEC分为两大部分,Reset Vector阶段,调用SEC入口地址进入SEC,启动Reset Vector会进入固件入口,从实模式转为32为平坦模式,定位固件中的BFV(Boot Frimware Volume),如果64位系统,从32位模式转为64位模式,调用SEC入口
- PEI(Pre-EFI Initialization)主要功能时为DXE准备执行环境,需要将DXE的信息组成
HOB
(Handoff Block)列表,最终将控制权交给DXE - DXE(Driver Execution Environment)执行系统初始化工作,在次阶段内存可以使用,DXE驱动通过Protocol通信,我们可以使用Protocol提供的服务,当所有的Driver执行完毕后,系统初始化完成,随后进入BDS阶段
- BDS(Boot Devices Selection)主要功能时执行启动策略,主要包括初始化控制台设备,加载设备驱动,根据系统设置加载和执行启动项,用于选中某个启动项后,OS Loader启动,系统进入TSL阶段
- TSL(Transient System Load)为系统加载器执行的第一个阶段,在这一阶段系统加载器作为UEFI应用程序运行,系统资源仍由UEFI内核控制,当启动服务调用ExitBootServer()后系统进入Runtime阶段
- RT(Run Time)系统的控制权从UEFI内核转给系统加载器中,UEFI占用的各种资源被回收到系统加载器中,仅保留UEFI运行服务和OS,最后OS取得最终控制权
- AL(After Life):在RT阶段如果系统遇到灾难性错误,系统固件需要提供错误处理和灾难恢复机制
引导管理
UEFI Image
UEFI映像是UEFI定义的一类文件,其中包含可执行代码。UEFI Image是一类包含可执行代码的文件,UEFI的区别在Image Header中的Magic Number不同
UEFI使用PE32的Image格式的子集PE32+,PE32+ Image中Image Header与普通的PE32可执行文件不同,"+"表示增加了对PE32格式的64位重定位的扩展,Image分为不同的类型,下表为不同架构的Image镜像类型
架构 | Machine Type |
---|---|
IA32 | 0x014c |
IA64 | 0x0200 |
EBC | 0x0EBC |
x64 | 0x8664 |
ARMTHUMB_MIXED | 0x01C2 |
AARCH64 | 0xAA64 |
RISCV32 | 0x5032 |
RISCV64 | 0x5064 |
RISCV128 | 0x5128 |
Image类型之间的区别是固件将Image加载到的内存类型,以及image的加载入口,退出或返回时所采取的操作当从映像的入口点返回控制权时,UEFI应用程序Image始终会被卸载。仅当使用UEFI错误代码传回控制时,才会卸载UEFI驱动程序映像。
UEFI映像通过EFI_BOOT_SERVICES.LoadImage()引导服务加载到内存中。该服务将PE32+格式的Image加载到内存中。PE32+加载程序将PE32+ Image的所有section加载到内存中。一旦将Image加载到内存中并进行适当的调整,随后在使用AddressOfEntryPointreference加载的映像,应用程序会根据所支持的32位,64位或128位处理器的调用约定运行
UEFI 应用程序
Boot Manager或其他UEFI应用程序可以加载按照规范编写的应用程序。 要加载UEFI应用程序,固件会为Application Image分配足够的内存,随后将Application Image中的section拷贝到固件所分配的内存中,根据需要进行重定位处理, 完成之后,根据image类型将被分配的内存设置CODE和DATA类型,然后将控制权转移到应用程序入口处当应用程序结束后返回,或者当它调用EFI_BOOT_SERVICES.Exit()时,UEFI应用程序将从内存中卸载,并将控制权返回给加载该UEFI应用程序的UEFI组件。
OS Loader
UEFI OS Loader是一种特殊类型的UEFI应用程序,通常会从固件中接管系统的控制权。加载后,UEFI OS加载程序的行为与任何其他UEFI应用程序相同,它只能使用从固件提供的内存分配释放功能,并且只能使用UEFI服务和协议来访问固件提供的可用的设备
如果UEFI OS加载程序遇到问题并且无法正确加载其操作系统,则它需要释放所有分配的资源,并通过Boot Service Exit()调用将控制权返回给固件。 Exit()调用允许同时返回错误代码和ExitData。 ExitData包含字符串和要返回的OS加载程序特定的数据
如果UEFI OS加载程序成功加载了其操作系统,则可以使用引导服务EFI_BOOT_SERVICES.ExitBootServices()来控制系统。调用成功后将停止系统中的所有引导服务,包括内存管理,并且由UEFI OS Loader负责系统的继续运行。
UEFI运行时服务
运行时服务的主要目的是从OS中抽象平台硬件实现的一小部分。运行时服务功能在引导过程中可用,并且在运行时也可用,只要OS切换到平坦物理寻址模式(虚拟地址=物理地址)即可使用运行时调用,但是,如果OS加载程序或OS使用SetVirtualAddressMap()服务,操作系统将只能以虚拟寻址模式调用运行时服务。所有运行时接口均为非阻塞接口,可以根据需要在禁用中断的情况下调用。在所有情况下,运行时服务使用的内存都必须保留,并且操作系统不应该使用。运行时服务内存始终可用于UEFI功能,并且不应该被OS或其组件直接操纵。UEFI负责定义运行时服务使用的硬件资源,因此OS可以在调用运行时服务时与这些资源同步,或者保证OS不会使用这些资源。
调用约定
UEFI规范中定义的所有功能都是由C编译器以及架构决定的,调用约定中的指针调用的。 在通过EFI_RUNTIME_SERVICES和EFI_BOOT_SERVICES表中找到各种全局UEFI功能的指针。 在所有情况下,所有指向UEFI功能的指针都使用EFIAPI进行强制转换。 这允许每种体系结构的编译器提供适当的编译器关键字,以实现所需的调用约定。 当将指针参数传递给引导服务,运行时服务和协议接口时,调用者具有以下职责:
- 调用者负责传递引用物理内存位置的指针。 如果传递的指针未指向物理内存位置(即内存映射的I / O区域),可能产生不可预测的问题,并且系统可能会因此暂停。
- 调用者负责传递经过正确对齐的指针。 如果将未对齐的指针传递给函数,可能产生不可预测的问题,并且系统可能会因此暂停。
- 除非明确允许,否则调用者不能将NULL当做参数传递给函数。 如果将NULL指针传递给函数,可能产生不可预测的问题,并且系统可能会因此暂停。
- 如果函数返回错误,则调用者不应对指针参数的状态做出任何假设。
- 调用者不得传递大于本机大小的值的结构,并且调用者必须按引用(通过指针)传递这些结构。 如果在堆栈上传递大于位宽(32位处理器上为4字节;64位处理器上为8字节)的结构将产生未知的结果。
任何功能或协议都可以返回任何有效的状态码。
UEFI模块的所有公共接口必须遵循UEFI调用约定。 公共接口包括Image入口点,UEFI事件处理程序和协议成员函数。 对于非公共接口(例如私有函数和静态库调用)不需要遵循UEFI调用约定,并且可以由编译器进行优化
调用约定的数据类型
除非另有说明,否则所有数据类型都是自然对齐的。 结构体类型对齐方式为该结构体最大数据成员基准的边界上对齐,并且隐式填充内部数据以实现自然对齐。UEFI接口传递或返回的指针的值必须为自然对齐。
标识符 | 描述 |
---|---|
BOOLEAN | |
INTN | 有符号值的宽度(32位处理器为4字节。 64位处理器为8字节。 128位处理器为16字节) |
UINTN | 无符号值的宽度。(32位处理器为4字节。 64位处理器为8字节。 128位处理器为16字节) |
INT8 | 1字节(有符号) |
UINT8 | 1字节(无符号) |
INT16 | 2字节(有符号) |
UINT16 | 2字节(无符号) |
INT32 | 4字节(有符号) |
UINT32 | 4字节(无符号) |
INT64 | 8字节(有符号) |
UINT64 | 8字节(无符号) |
INT128 | 16字节(有符号) |
UINT128 | 16字节(无符号) |
CHAR8 | 1字节的字符。除非另有说明,否则所有1字节或ASCII字符和字符串均使用ISO-Latin-1字符集以8位ASCII编码格式存储. |
CHAR16 | 2字节字符。除非另有说明,否则所有字符和字符串都以Unicode 2.1和ISO / IEC 10646标准定义的UCS-2编码格式存储。 |
VOID | 未声明的类型 |
EFI_GUID | 包含唯一标识符的128位缓冲区。除非另有说明,否则在64位边界上对齐。 |
EFI_STATUS | 状态码。类型为UINTN |
EFI_HANDLE | 相关接口的集合。类型为 VOID *。 |
EFI_EVENT | 处理事件结构。类型为VOID *. |
EFI_LBA | 逻辑块地址。类型为UINT64. |
EFI_TPL | 任务优先级。类型为UINTN. |
EFI_MAC_ADDRESS | 包含网络访问控制地址的32字节缓冲区。 |
EFI_IPv4_ADDRESS | 4字节缓冲区。 IPv4互联网协议地址。 |
EFI_IPv6_ADDRESS | 16字节缓冲区。 IPv6互联网协议地址。 |
EFI_IP_ADDRESS | 16字节缓冲区在4字节边界上对齐。 IPv4或IPv6 Internet协议地址。 |
<枚举类型> | 标准ANSI C枚举类型声明的元素。类型为INT32或UINT32,ANSI C没有定义枚举的符号大小,因此切勿在结构中使用它们,作为参数传递给函数时,ANSI C可以使INT32或UINT32可互换。 |
sizeof(VOID *) | 32位处理器为4字节。 64位处理器为8字节。 128位处理器为16字节。 |
位域 | 位域的排序方式是使位0为最低有效位。 |
IA-32架构调用约定
所有函数都使用C语言调用约定来调用。 跨函数调用易失性通用寄存器是eax,ecx和edx。 所有其他通用寄存器都是非易失性的,并由目标函数保留。 此外,除非函数定义另有规定,否则将保留所有其他寄存器。
在OS调用ExitBootServices()之前,固件启动服务和运行时服务以以下处理器执行模式运行:
- 单处理器模式可参考英特尔64和IA-32架构软件开发人员手册第3卷第8.4章
- 系统运行在64位长模式
- 启用了分页模式,UEFI所定义内存空间都进行了映射(虚拟地址等于物理地址),尽管某些区域的属性可能没有全部读取,写入和执行属性,也可能没有标记以进行平台保护 。到其他区域的映射是不确定的,并且可能因实现方式而异。
- 选择子设置为flat,否则不使用。
- 启用了中断-UEFI引导服务计时器功能除外,但不支持任何中断服务(所有加载的设备驱动程序均通过“轮询”进行同步服务。)
- EFLAG中的DF标记已复位
- 其他通用标志寄存器未定义
- 128KB或更多可用堆栈空间
- 堆栈必须对齐16字节。 堆栈在映射中可能被标记为
NO_EXECUTE
页表。 - 浮点控制字必须初始化为0x037F(所有带掩码的例外,双精度扩展)
- 多媒体扩展控制字(如果支持)必须初始化为0x1F80
- CR0.EM必须为零
- CR0.TS必须为零
为了使操作系统使用任何UEFI运行时服务,必须保证以下状态:
- 保留内存映射中标记为
RUNTIME_CODE
和RUNTIME_DATA
的所有内存 - 在64位长模式下
- 启用分页
- 如果UEFI OS加载程序或OS使用SetVirtualAddressMap()在虚拟地址空间中重新定位运行时服务,则不必满足此条件。
- EFLAG中的DF标记已复位
- 4KB以上可用的堆栈空间
- 堆栈必须对齐16字节
- 浮点控制字必须初始化为0x037F
- CR0.EM必须为零
- CR0.TS必须为零
- 调用者可以自行决定是否禁用或启用中断。
- 引导时加载的ACPI表可以包含EfiACPIReclaimMemory(推荐)或EfiACPIMemoryNVS类型。 ACPI FACS必须包含EfiACPIMemoryNVS类型。
- 系统固件不得为EfiACPIReclaimMemory或EfiACPIMemoryNVS类型的任何内存描述符进行虚拟映射
- EfiACPIReclaimMemory和EfiACPIMemoryNVS类型的EFI内存描述符必须在4 KB边界上对齐,并且必须是4 KB的倍数。
- 设置了EFI_MEMORY_RUNTIME位的EFI_MEMORY_DESCRIPTOR虚拟映射的任何UEFI内存描述符都必须在4 KB边界上对齐,并且大小必须为4 KB的倍数。
Rust中的UEFI
我们使用的是rust-osdev
的uefi-rs
库,rust-osdev
为Rust提供了x86_64-unknown-uefi编译目标,因此我们只需要指定编译目标即可编译成.efi
文件,我们使用的UEFI标准实现是EDK2,如果使用QEMU来启动或调试需要使用OVMF
(开放虚拟机固件)
UEFI的基础服务如下
- 系统表: 系统表提供了用户空间与内核空间的通道(UEFI内核)UEFI应用程序和驱动通过系统表才能访问硬件资源和IO设备
- 启动服务:在系统启动过程中,系统资源通过启动服务来管理,系统进入DXE阶段时启动服务表,系统服务分为以下几类
- UEFI事件服务:有了事件才能在UEFI系统内执行异步并发操作
- 内存管理服务:主要提供内存分配与释放,管理系统内存映射
- Protocol管理服务:提供Protocol安装,注册和卸载
- Protocol使用服务:Protocol的打开与关闭
- 驱动管理服务: 提供驱动的安装卸载服务
- Image服务i,包括加载,卸载,启动,退出UEFI应用程序或驱动
- ExitBootService:用于结束启动服务,执行成功后系统进入RT阶段
- 其他服务
- 运行时服务:从进入DXE阶段运行时服务被初始化,直到操作系统结束
- 时间服务:读取/设定系统时间,读取设定系统从睡眠中唤醒的时间
- 读写内存变量:读取设置系统变量,例如指定启动项顺序
- 虚拟内存服务:将物理地址转为虚拟地址
- 其他服务
UEFI入口
在Rust中SXE入口声明如下
#![no_std]
#![no_main]
#![feature(asm)]
#![feature(slice_patterns)]
#![feature(abi_efiapi)]
use uefi::prelude::*;#[entry]
fn efi_main(image: Handle, st: SystemTable<Boot>) -> Status {// 初始化uefi_services::init(&st).expect_success("Failed to initialize utilities");....
}
efi_main
函数相当于普通应用程序的main
函数,在进入入口后我们需要对UEFI服务进行初始化,初始化完毕后我们可以
OVMF固件制作
- 下载EDK2
$ git clone https://github.com/tianocore/edk2.git
$ cd edk2
// EDK2有一些依赖库比如openssl等
$ git submodule update --init
- 指定编译平台,在edk2/Conf/target.txt更改平台
例如Ubuntu(64)
ACTIVE_PLATFORM = EmulatorPkg/EmulatorPkg.dsc
TARGET = DEBUG # 编译目标
TARGET_ARCH = IA32 # 目标平台
TOOL_CHAIN_CONF = Conf/tools_def.txt # 工具配置文件
TOOL_CHAIN_TAG = GCC5 # 使用编译器 MVSC 支持MSVC
# MAX_CONCURRENT_THREAD_NUMBER = 1
BUILD_RULE_CONF = Conf/build_rule.txt # 构建规则文件
- 编译EDK2工具链 安装依赖环境
sudo apt-get install build-essential uuid-dev
- 开始编译
edk2$ cd BaseTools
edk2/BaseTools$ make
// 编译完毕后Source以下
edk2$ source edksetup.sh
- 编译OVMF 编译64位固件
edk2$ build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5
- 编译后在
edk2/Build/Ovmfla32/DEBUG_GCC5/FV
下面会生成OVMF.fd
,OVMF_CODE.fd
,OVMF_VARS.fd
等文件这些文件我们后面会使用到
Protocol
Protocol是一种约定,可以通过BootServices.locate_protocol()
来获取对应的Protocol,每个Protocol必须有一个唯一的UUID,每一个Protocol提供了一种功能,例如
#[repr(C)]
#[unsafe_guid("964e5b22-6459-11d2-8e39-00a0c969723b")]
#[derive(Protocol)]
pub struct SimpleFileSystem {revision: u64,open_volume: extern "efiapi" fn(this: &mut SimpleFileSystem, root: &mut *mut FileImpl) -> Status,
}
964e5b22-6459-11d2-8e39-00a0c969723b
就是SimpleFileSystemProtocol的UUID, SimpleFileSystem可以访问FAT-12 / 16/32等文件系统,后续我们会介绍各种各样的Protocol
UEFI启动系统过程
我们要通过UEFI启动系统需要经过以下步骤
- 制作系统固件,我们将代码最终编译成
.efi
文件,在执行前需要建立一个efi/boot
目录并且使用将.efi
文件放入efi/boot
文件夹中,如果我们使用的是uefi shell(QEMU中直接使用OVMF进入UEFI shell)在执行.efi
文件后会把.efi
文件加载内存中生成Image对象,然后启动这个Image对象,在启动Image对象时将会找出Image的入口并执行入口函数, - 进入到执行入口后我们需要对基本的服务进行初始化,初始化完毕后我们需要检测系统的运行环境并收集系统所需的参数
- 随后我们使用SimpleFileSystem来寻找系统内核文件并加载到内存中,解析内核文件找到内核入口
- 最后我们调用ExitBootService结束启动过程,跳转到内核入口
虽然uefi-rs
的支持的功能不是特别多但是足以满足我们系统的使用
下一步要做什么
在下一篇文章中我们介绍uefi-rs
的及基本数据结构以及对应的使用方式(uefi-rs
的文档比较欠缺)为我们加载内核做准备
使用Rust开发操作系统(UEFI基本介绍)相关推荐
- C 和 C++ 不安全?Android 支持 Rust 开发操作系统
作者 | Carol 出品 | CSDN(ID:CSDNnews) Rust这两年实火了. 近年来,Rust凭借着出色的内存效率.速度与安全性,深受亚马逊.微软.华为.Facebook等科技巨头的青睐 ...
- 百度安全 TrustZone SDK 正式成为 OP-TEE 官方推荐 Rust 开发环境
百度安全与 OP-TEE 社区共同合作,推进 Teacalve TrustZone SDK 正式成为 OP-TEE 官方推荐的 Rust 开发环境.OP-TEE 是当今广泛使用的开源 ARM Trus ...
- 用 Rust 开发 Linux,可行吗?
作者 | 马超 出品 | CSDN(ID:CSDNnews) 继Python之后,Rust最近也火爆得出了圈,目前Rust在Serverless等很多云原生领域已经稳定占据了C位,那么让Rust更进一 ...
- 高通about.html 文件,高通平台UEFI有关介绍
高通平台UEFI有关介绍 背景 我需要在高通平台上学习点亮LCD,目前通过同事在别的平台的配置代码,我已经将kernel部分的屏幕点亮了:剩余的工作量就在BP侧,也就是系统刚开机的那一段时间.在开发过 ...
- 在Windows上搭建Rust开发环境——Clion篇
文章目录 在Windows上搭建Rust开发环境--Clion篇 安装mingw64 安装Rust hello world 安装Clion 使用Clion创建并调试项目 在Windows上搭建Rust ...
- Linux下服务器端开发流程及相关工具介绍(C++)
原文:Linux下服务器端开发流程及相关工具介绍(C++) 去年刚毕业来公司后,做为新人,发现很多东西都没有文档,各种工具和地址都是口口相传的,而且很多时候都是不知道有哪些工具可以使用,所以当时就想把 ...
- Xamarin Anroid开发教程之Anroid开发工具及应用介绍
Xamarin Anroid开发教程之Anroid开发工具及应用介绍 Xamarin开发Anroid应用介绍 如今智能手机已经盛行了好几年,而针对这些智能手机的软件开发也变得异常火热.但是在Andro ...
- (第五篇)Linux操作系统基本结构介绍
Linux操作系统基本结构介绍 Linux系统一般有4个主要部分:内核.shell.文件系统和应用程序.内核.shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序.管理文件并使用 ...
- rust开发环境_Rust 环境搭建
Rust 环境搭建 Rust 支持很多的集成开发环境(IDE)或开发专用的文本编辑器. 本教程将使用 Visual Studio Code 作为我们的开发环境(Eclipse 有专用于 Rust 开发 ...
最新文章
- pycharm如何分两页阅读_雅思高分喜报| 听力、阅读、写作突破7分,2个月轻松突破雅思总分7, 她是如何做到的?...
- php运算符的特殊用法
- jzoj1013-GCD与LCM【数论】
- 认识高清视频编码(MPEG、H.264、WMV-HD、RMVB)
- threadlocal get为空_面试常见知识点:ThreadLocal
- python传递参数 调用c++ 传递vector_python调用c++传递数组的实例
- 获取Django中model字段名 字段的verbose_name
- MySQL 数据库笔记
- 课堂笔记(一些知识散点)但很有用(*^__^*)
- maven 生命周期的指令
- 6.0后,全局悬浮窗或者弹窗不显示的解决办法
- python培训全套免费教程百度云202
- 博弈论个人的一点小总结
- 成功粉碎北信源监控程序vrvedp_m.exe ,vrvrf_c64.exe,svchost.exe,vrvrf_c.exe
- 坦克大战的网络对战实现C++(客户端+服务端)
- 基于Vue的WebApp项目开发(五)
- 读《大型网站技术架构:核心原理与案例分析+李智慧》记一
- Mysql(三)索引、视图、存储过程、触发器、分区表
- 15分钟详解 Python 安全认证的那些事儿~
- java json parser_Java JSONParser.parse方法代碼示例
热门文章
- h5 vue利用canvas实现手机签名并且可旋转功能
- 【无人机设计与开发】推荐几个无人机网址
- 新旧CAD图纸对比-用BCore图纸引擎1秒就能完成
- 【Unity】Obi插件系列(六)—— Obi Cloth
- android 项目编译报错 符号: 类 shape 位置: 程序包 android.widget
- 求解线性方程组的方法Matlab程序
- 送你4句口诀 云存储选型不再犯难
- 【ZT】iSpring Free Cam – 免费易用的「录制屏幕」工具 [Windows]
- IP解析成地址 确定省市
- waf 防火墙限制_WAF防火墙设备指标及参数说明