之前也写过一个vt的框架,但是比较简单,写的比较乱迁移什么的比较麻烦,于是阅读下HyperPlatform的源码学习下。

本文只对主体框架分析。

vt的流程大概如下

1:检测是否支持VT。

2:vmxon。

3:vmclear.

4:vmptrload。

5:vmcs。

6:vmlaunch

7:vmclear

8:vmoff

VmInitialization

首先是关键的VMinit

检测vt是否已被占用

Use_decl_annotations_ static bool VmpIsHyperPlatformInstalled() {
PAGED_CODE() int cpu_info[4] = {};
__cpuid(cpu_info, 1);
const CpuFeaturesEcx cpu_features = {static_cast<ULONG32>(cpu_info[2])};
if (!cpu_features.fields.not_used) {
return false;
} __cpuid(cpu_info, kHyperVCpuidInterface);
return cpu_info[0] == 'PpyH';
}

检测是否支持VT

// Checks if the system supports virtualization
_Use_decl_annotations_ static bool VmpIsVmxAvailable() {
PAGED_CODE() // See: DISCOVERING SUPPORT FOR VMX
// If CPUID.1:ECX.VMX[bit 5]=1, then VMX operation is supported.
int cpu_info[4] = {};
__cpuid(cpu_info, 1);
const CpuFeaturesEcx cpu_features = {static_cast<ULONG32>(cpu_info[2])};
if (!cpu_features.fields.vmx) {
HYPERPLATFORM_LOG_ERROR("VMX features are not supported.");
return false;
}
......
Ia32FeatureControlMsr vmx_feature_control = {
UtilReadMsr64(Msr::kIa32FeatureControl)};
if (!vmx_feature_control.fields.enable_vmxon) {
HYPERPLATFORM_LOG_ERROR("VMX features are not enabled.");
return false;
}
......
return true;
}

检查都通过后进入正题调用VmpStartVm

VmpStartVm

VmpInitializeVm

分配VMXON内存

  processor_data->vmxon_region =
static_cast<VmControlStructure *>(ExAllocatePoolZero(
NonPagedPool, kVmxMaxVmcsSize, kHyperPlatformCommonPoolTag));
if (!processor_data->vmxon_region) {
VmpFreeProcessorData(processor_data);
return;
}
RtlZeroMemory(processor_data->vmxon_region, kVmxMaxVmcsSize);

分配VMCS内存

  processor_data->vmcs_region =
static_cast<VmControlStructure *>(ExAllocatePoolZero(
NonPagedPool, kVmxMaxVmcsSize, kHyperPlatformCommonPoolTag));
if (!processor_data->vmcs_region) {
VmpFreeProcessorData(processor_data);
return;
}
RtlZeroMemory(processor_data->vmcs_region, kVmxMaxVmcsSize);

分配VMM stack内存

processor_data->vmm_stack_limit =
UtilAllocateContiguousMemory(KERNEL_STACK_SIZE);
if (!processor_data->vmm_stack_limit) {
VmpFreeProcessorData(processor_data);
return;
}
RtlZeroMemory(processor_data->vmm_stack_limit, KERNEL_STACK_SIZE);

VmpEnterVmxMode

对应VMXON,修正cr0和cr4寄存器的值,在上面申请的VMXON内存中写入版本号,然后执行vmxon。

_Use_decl_annotations_ static bool VmpEnterVmxMode(
ProcessorData *processor_data) {
PAGED_CODE() // Apply FIXED bits
// See: VMX-FIXED BITS IN CR0 // IA32_VMX_CRx_FIXED0 IA32_VMX_CRx_FIXED1 Meaning
// Values 1 * bit of CRx is fixed to 1
// Values 0 1 bit of CRx is flexible
// Values * 0 bit of CRx is fixed to 0
const Cr0 cr0_fixed0 = {UtilReadMsr(Msr::kIa32VmxCr0Fixed0)};
const Cr0 cr0_fixed1 = {UtilReadMsr(Msr::kIa32VmxCr0Fixed1)};
Cr0 cr0 = {__readcr0()};
Cr0 cr0_original = cr0;
cr0.all &= cr0_fixed1.all;
cr0.all |= cr0_fixed0.all;
__writecr0(cr0.all); HYPERPLATFORM_LOG_DEBUG("IA32_VMX_CR0_FIXED0 = %08Ix", cr0_fixed0.all);
HYPERPLATFORM_LOG_DEBUG("IA32_VMX_CR0_FIXED1 = %08Ix", cr0_fixed1.all);
HYPERPLATFORM_LOG_DEBUG("Original CR0 = %08Ix", cr0_original.all);
HYPERPLATFORM_LOG_DEBUG("Fixed CR0 = %08Ix", cr0.all); // See: VMX-FIXED BITS IN CR4
const Cr4 cr4_fixed0 = {UtilReadMsr(Msr::kIa32VmxCr4Fixed0)};
const Cr4 cr4_fixed1 = {UtilReadMsr(Msr::kIa32VmxCr4Fixed1)};
Cr4 cr4 = {__readcr4()};
Cr4 cr4_original = cr4;
cr4.all &= cr4_fixed1.all;
cr4.all |= cr4_fixed0.all;
__writecr4(cr4.all); HYPERPLATFORM_LOG_DEBUG("IA32_VMX_CR4_FIXED0 = %08Ix", cr4_fixed0.all);
HYPERPLATFORM_LOG_DEBUG("IA32_VMX_CR4_FIXED1 = %08Ix", cr4_fixed1.all);
HYPERPLATFORM_LOG_DEBUG("Original CR4 = %08Ix", cr4_original.all);
HYPERPLATFORM_LOG_DEBUG("Fixed CR4 = %08Ix", cr4.all); // Write a VMCS revision identifier
const Ia32VmxBasicMsr vmx_basic_msr = {UtilReadMsr64(Msr::kIa32VmxBasic)};
processor_data->vmxon_region->revision_identifier =
vmx_basic_msr.fields.revision_identifier; auto vmxon_region_pa = UtilPaFromVa(processor_data->vmxon_region);
if (__vmx_on(&vmxon_region_pa)) {
return false;
} // See: Guidelines for Use of the INVVPID Instruction, and Guidelines for Use
// of the INVEPT Instruction
UtilInveptGlobal();
UtilInvvpidAllContext();
return true;
}

VmpInitializeVmcs

对应vmclear和vmptrload。向上面申请的VMCS内存区写入版本号,然后执行__vmx_vmclear和__vmx_vmptrld。

_Use_decl_annotations_ static bool VmpInitializeVmcs(
ProcessorData *processor_data) {
PAGED_CODE() // Write a VMCS revision identifier
const Ia32VmxBasicMsr vmx_basic_msr = {UtilReadMsr64(Msr::kIa32VmxBasic)};
processor_data->vmcs_region->revision_identifier =
vmx_basic_msr.fields.revision_identifier; auto vmcs_region_pa = UtilPaFromVa(processor_data->vmcs_region);
if (__vmx_vmclear(&vmcs_region_pa)) {
return false;
}
if (__vmx_vmptrld(&vmcs_region_pa)) {
return false;
} // The launch state of current VMCS is "clear"
return true;
}

VmpSetupVmcs

对应VMCS。这也是整个框架中最复杂的地方。这一步主要的目的是设置环境以便使虚拟机可以正常运行和退出。

太长了不全分析。

设置GUEST区域

  error |= UtilVmWrite(VmcsField::kGuestEsSelector, AsmReadES());
error |= UtilVmWrite(VmcsField::kGuestCsSelector, AsmReadCS());
error |= UtilVmWrite(VmcsField::kGuestSsSelector, AsmReadSS());
error |= UtilVmWrite(VmcsField::kGuestDsSelector, AsmReadDS());
error |= UtilVmWrite(VmcsField::kGuestFsSelector, AsmReadFS());
error |= UtilVmWrite(VmcsField::kGuestGsSelector, AsmReadGS());
error |= UtilVmWrite(VmcsField::kGuestLdtrSelector, AsmReadLDTR());
error |= UtilVmWrite(VmcsField::kGuestTrSelector, AsmReadTR());
......
error |= UtilVmWrite64(VmcsField::kVmcsLinkPointer, MAXULONG64);
error |= UtilVmWrite64(VmcsField::kGuestIa32Debugctl, UtilReadMsr64(Msr::kIa32Debugctl));
.......
error |= UtilVmWrite(VmcsField::kGuestEsLimit, GetSegmentLimit(AsmReadES()));
error |= UtilVmWrite(VmcsField::kGuestCsLimit, GetSegmentLimit(AsmReadCS()));
error |= UtilVmWrite(VmcsField::kGuestSsLimit, GetSegmentLimit(AsmReadSS()));
error |= UtilVmWrite(VmcsField::kGuestDsLimit, GetSegmentLimit(AsmReadDS()));
error |= UtilVmWrite(VmcsField::kGuestFsLimit, GetSegmentLimit(AsmReadFS()));
error |= UtilVmWrite(VmcsField::kGuestGsLimit, GetSegmentLimit(AsmReadGS()));
error |= UtilVmWrite(VmcsField::kGuestLdtrLimit, GetSegmentLimit(AsmReadLDTR()));
error |= UtilVmWrite(VmcsField::kGuestTrLimit, GetSegmentLimit(AsmReadTR()));
error |= UtilVmWrite(VmcsField::kGuestGdtrLimit, gdtr.limit);
error |= UtilVmWrite(VmcsField::kGuestIdtrLimit, idtr.limit);
error |= UtilVmWrite(VmcsField::kGuestEsArBytes, VmpGetSegmentAccessRight(AsmReadES()));
error |= UtilVmWrite(VmcsField::kGuestCsArBytes, VmpGetSegmentAccessRight(AsmReadCS()));
error |= UtilVmWrite(VmcsField::kGuestSsArBytes, VmpGetSegmentAccessRight(AsmReadSS()));
error |= UtilVmWrite(VmcsField::kGuestDsArBytes, VmpGetSegmentAccessRight(AsmReadDS()));
error |= UtilVmWrite(VmcsField::kGuestFsArBytes, VmpGetSegmentAccessRight(AsmReadFS()));
error |= UtilVmWrite(VmcsField::kGuestGsArBytes, VmpGetSegmentAccessRight(AsmReadGS()));
error |= UtilVmWrite(VmcsField::kGuestLdtrArBytes, VmpGetSegmentAccessRight(AsmReadLDTR()));
error |= UtilVmWrite(VmcsField::kGuestTrArBytes, VmpGetSegmentAccessRight(AsmReadTR()));
error |= UtilVmWrite(VmcsField::kGuestSysenterCs, UtilReadMsr(Msr::kIa32SysenterCs));
......
error |= UtilVmWrite(VmcsField::kCr0GuestHostMask, cr0_mask.all);
error |= UtilVmWrite(VmcsField::kCr4GuestHostMask, cr4_mask.all);
error |= UtilVmWrite(VmcsField::kCr0ReadShadow, cr0_shadow.all);
error |= UtilVmWrite(VmcsField::kCr4ReadShadow, cr4_shadow.all);
......
error |= UtilVmWrite(VmcsField::kGuestCr0, __readcr0());
error |= UtilVmWrite(VmcsField::kGuestCr3, __readcr3());
error |= UtilVmWrite(VmcsField::kGuestCr4, __readcr4());
......
error |= UtilVmWrite(VmcsField::kGuestEsBase, 0);
error |= UtilVmWrite(VmcsField::kGuestCsBase, 0);
error |= UtilVmWrite(VmcsField::kGuestSsBase, 0);
error |= UtilVmWrite(VmcsField::kGuestDsBase, 0);
error |= UtilVmWrite(VmcsField::kGuestFsBase, UtilReadMsr(Msr::kIa32FsBase));
error |= UtilVmWrite(VmcsField::kGuestGsBase, UtilReadMsr(Msr::kIa32GsBase));
......
error |= UtilVmWrite(VmcsField::kGuestLdtrBase, VmpGetSegmentBase(gdtr.base, AsmReadLDTR()));
error |= UtilVmWrite(VmcsField::kGuestTrBase, VmpGetSegmentBase(gdtr.base, AsmReadTR()));
error |= UtilVmWrite(VmcsField::kGuestGdtrBase, gdtr.base);
error |= UtilVmWrite(VmcsField::kGuestIdtrBase, idtr.base);
error |= UtilVmWrite(VmcsField::kGuestDr7, __readdr(7));
error |= UtilVmWrite(VmcsField::kGuestRsp, guest_stack_pointer);
error |= UtilVmWrite(VmcsField::kGuestRip, guest_instruction_pointer);
error |= UtilVmWrite(VmcsField::kGuestRflags, __readeflags());
error |= UtilVmWrite(VmcsField::kGuestSysenterEsp, UtilReadMsr(Msr::kIa32SysenterEsp));
error |= UtilVmWrite(VmcsField::kGuestSysenterEip, UtilReadMsr(Msr::kIa32SysenterEip));

设置HOST区域

  error |= UtilVmWrite(VmcsField::kHostEsSelector, AsmReadES() & 0xf8);
error |= UtilVmWrite(VmcsField::kHostCsSelector, AsmReadCS() & 0xf8);
error |= UtilVmWrite(VmcsField::kHostSsSelector, AsmReadSS() & 0xf8);
error |= UtilVmWrite(VmcsField::kHostDsSelector, AsmReadDS() & 0xf8);
error |= UtilVmWrite(VmcsField::kHostFsSelector, AsmReadFS() & 0xf8);
error |= UtilVmWrite(VmcsField::kHostGsSelector, AsmReadGS() & 0xf8);
error |= UtilVmWrite(VmcsField::kHostTrSelector, AsmReadTR() & 0xf8);
......
error |= UtilVmWrite(VmcsField::kHostCr0, __readcr0());
error |= UtilVmWrite(VmcsField::kHostCr3, __readcr3());
error |= UtilVmWrite(VmcsField::kHostCr4, __readcr4());
......
error |= UtilVmWrite(VmcsField::kHostFsBase, UtilReadMsr(Msr::kIa32FsBase));
error |= UtilVmWrite(VmcsField::kHostGsBase, UtilReadMsr(Msr::kIa32GsBase));
......
error |= UtilVmWrite(VmcsField::kHostIa32SysenterCs, UtilReadMsr(Msr::kIa32SysenterCs));
......
error |= UtilVmWrite(VmcsField::kHostTrBase, VmpGetSegmentBase(gdtr.base, AsmReadTR()));
error |= UtilVmWrite(VmcsField::kHostGdtrBase, gdtr.base);
error |= UtilVmWrite(VmcsField::kHostIdtrBase, idtr.base);
error |= UtilVmWrite(VmcsField::kHostIa32SysenterEsp, UtilReadMsr(Msr::kIa32SysenterEsp));
error |= UtilVmWrite(VmcsField::kHostIa32SysenterEip, UtilReadMsr(Msr::kIa32SysenterEip));
error |= UtilVmWrite(VmcsField::kHostRsp, vmm_stack_pointer);
error |= UtilVmWrite(VmcsField::kHostRip, reinterpret_cast<ULONG_PTR>(AsmVmmEntryPoint));

设置control域

  error |= UtilVmWrite64(VmcsField::kIoBitmapA, UtilPaFromVa(processor_data->shared_data->io_bitmap_a));
error |= UtilVmWrite64(VmcsField::kIoBitmapB, UtilPaFromVa(processor_data->shared_data->io_bitmap_b));
error |= UtilVmWrite64(VmcsField::kMsrBitmap, UtilPaFromVa(processor_data->shared_data->msr_bitmap));
error |= UtilVmWrite64(VmcsField::kEptPointer, EptGetEptPointer(processor_data->ept_data));

......

error |= UtilVmWrite(VmcsField::kPinBasedVmExecControl, vm_pinctl.all);

error |= UtilVmWrite(VmcsField::kCpuBasedVmExecControl, vm_procctl.all);

error |= UtilVmWrite(VmcsField::kExceptionBitmap, exception_bitmap);

error |= UtilVmWrite(VmcsField::kSecondaryVmExecControl, vm_procctl2.all);

......

设置entry域

  error |= UtilVmWrite(VmcsField::kVmEntryControls, vm_entryctl.all);

设置exit域

  error |= UtilVmWrite(VmcsField::kVmExitControls, vm_exitctl.all);

VmpLaunchVm

对应vmlaunch

_Use_decl_annotations_ static void VmpLaunchVm() {
PAGED_CODE() auto error_code = UtilVmRead(VmcsField::kVmInstructionError);
if (error_code) {
HYPERPLATFORM_LOG_WARN("VM_INSTRUCTION_ERROR = %Iu", error_code);
} auto vmx_status = static_cast<VmxStatus>(__vmx_vmlaunch()); // Here should not executed with successful vmlaunch. Instead, the context
// jumps to an address specified by GUEST_RIP.
if (vmx_status == VmxStatus::kErrorWithStatus) {
error_code = UtilVmRead(VmcsField::kVmInstructionError);
HYPERPLATFORM_LOG_ERROR("VM_INSTRUCTION_ERROR = %Iu", error_code);
}
HYPERPLATFORM_COMMON_DBG_BREAK();
}

至此启动vm。

AsmVmmEntryPoint

VM处理程序,没什么说的主要就是保存寄存器然后调用VmmVmExitHandler函数去处理vm-exit。

void __stdcall AsmVmmEntryPoint();
AsmVmmEntryPoint PROC FRAME
.PUSHFRAME
sub rsp, KTRAP_FRAME_SIZE - MACHINE_FRAME_SIZE
.ALLOCSTACK KTRAP_FRAME_SIZE - MACHINE_FRAME_SIZE + 108h ; No need to save the flag registers since it is restored from the VMCS at
; the time of vmresume.
PUSHAQ ; -8 * 16
mov rcx, rsp ; save the "stack" parameter for VmmVmExitHandler ; save volatile XMM registers
sub rsp, 68h ; 8 for alignment
movaps xmmword ptr [rsp + 0h], xmm0
movaps xmmword ptr [rsp + 10h], xmm1
movaps xmmword ptr [rsp + 20h], xmm2
movaps xmmword ptr [rsp + 30h], xmm3
movaps xmmword ptr [rsp + 40h], xmm4
movaps xmmword ptr [rsp + 50h], xmm5 sub rsp, 20h ; All stack allocation is done now. Indicate the end of prologue as required
; by the FRAME attribute.
.ENDPROLOG call VmmVmExitHandler ; bool vm_continue = VmmVmExitHandler(stack);
add rsp, 20h ; restore XMM registers
movaps xmm0, xmmword ptr [rsp + 0h]
movaps xmm1, xmmword ptr [rsp + 10h]
movaps xmm2, xmmword ptr [rsp + 20h]
movaps xmm3, xmmword ptr [rsp + 30h]
movaps xmm4, xmmword ptr [rsp + 40h]
movaps xmm5, xmmword ptr [rsp + 50h]
add rsp, 68h test al, al
jz exitVm ; if (!vm_continue) jmp exitVm POPAQ
vmresume
jmp vmxError exitVm:
; Executes vmxoff and ends virtualization
; rax = Guest's rflags
; rdx = Guest's rsp
; rcx = Guest's rip for the next instruction
POPAQ
vmxoff
jz vmxError ; if (ZF) jmp
jc vmxError ; if (CF) jmp
push rax
popfq ; rflags <= GurstFlags
mov rsp, rdx ; rsp <= GuestRsp
push rcx
ret ; jmp AddressToReturn vmxError:
; Diagnose a critical error
pushfq
PUSHAQ ; -8 * 16
mov rcx, rsp ; all_regs sub rsp, 28h ; 28h for alignment
call VmmVmxFailureHandler ; VmmVmxFailureHandler(all_regs);
add rsp, 28h
int 3
AsmVmmEntryPoint ENDP

VmmVmExitHandler

调用VmmpHandleVmExit处理。

用一个switch结构去处理exit-reason。

 switch (exit_reason.fields.reason) {
case VmxExitReason::kExceptionOrNmi:
VmmpHandleException(guest_context);
break;
case VmxExitReason::kTripleFault:
VmmpHandleTripleFault(guest_context);
/* UNREACHABLE */
case VmxExitReason::kCpuid:
VmmpHandleCpuid(guest_context);
break;
case VmxExitReason::kInvd:
VmmpHandleInvalidateInternalCaches(guest_context);
break;
case VmxExitReason::kInvlpg:
VmmpHandleInvalidateTlbEntry(guest_context);
break;
case VmxExitReason::kRdtsc:
VmmpHandleRdtsc(guest_context);
break;
case VmxExitReason::kCrAccess:
VmmpHandleCrAccess(guest_context);
break;
case VmxExitReason::kDrAccess:
VmmpHandleDrAccess(guest_context);
break;
case VmxExitReason::kIoInstruction:
VmmpHandleIoPort(guest_context);
break;
case VmxExitReason::kMsrRead:
VmmpHandleMsrReadAccess(guest_context);
break;
case VmxExitReason::kMsrWrite:
VmmpHandleMsrWriteAccess(guest_context);
break;
case VmxExitReason::kMonitorTrapFlag:
VmmpHandleMonitorTrap(guest_context);
/* UNREACHABLE */
case VmxExitReason::kGdtrOrIdtrAccess:
VmmpHandleGdtrOrIdtrAccess(guest_context);
break;
case VmxExitReason::kLdtrOrTrAccess:
VmmpHandleLdtrOrTrAccess(guest_context);
break;
case VmxExitReason::kEptViolation:
VmmpHandleEptViolation(guest_context);
break;
case VmxExitReason::kEptMisconfig:
VmmpHandleEptMisconfig(guest_context);
/* UNREACHABLE */
case VmxExitReason::kVmcall:
VmmpHandleVmCall(guest_context);
break;
case VmxExitReason::kVmclear:
case VmxExitReason::kVmlaunch:
case VmxExitReason::kVmptrld:
case VmxExitReason::kVmptrst:
case VmxExitReason::kVmread:
case VmxExitReason::kVmresume:
case VmxExitReason::kVmwrite:
case VmxExitReason::kVmoff:
case VmxExitReason::kVmon:
case VmxExitReason::kInvept:
case VmxExitReason::kInvvpid:
VmmpHandleVmx(guest_context);
break;
case VmxExitReason::kRdtscp:
VmmpHandleRdtscp(guest_context);
break;
case VmxExitReason::kXsetbv:
VmmpHandleXsetbv(guest_context);
break;
default:
VmmpHandleUnexpectedExit(guest_context);
/* UNREACHABLE */

如果想要自己增加处理的话在这个结构里注册就可以。

HyperPlatform的更多相关文章

  1. VT 入门番外篇——初识 VT

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

随机推荐

  1. 2021年9月学科能力综合测试(TACA)试题解答 Mathemaitca练习

    各个都是人形计算器???? 目录 试题地址 1 签到 2 3 签到 4 5 6 7 8 9 10 11 你让我猜我肯定这么猜 12 13 试题地址 http://www.mxqe.com/gzsnj/ ...

  2. Java面试——Java基础

    更多内容,移步IT-BLOG 一.JAVA中的几种基本数据类型 Java语言中一共提供了8种原始的数据类型(byte,short,int,long,float,double,char,boolean) ...

  3. MyBatis 重点知识归纳

    一.MyBatis 简介 [1]MyBatis 是支持定制化 SQL,存储过程以及高级映射的优秀持久化框架.[2]MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取查询结果集.[3 ...

  4. LevelDb-基本数据结构

    目录 Slice Arena skip list 跳表本质 时空复杂度 插入,删除数据(如何维护索引) 极端情况分析:不维护索引 极端情况分析:每次插入都维护 插入效率和查找效率取舍 删除 对比红黑树 ...

  5. Vue-router与hash与history区别

    vue-router 基本使用   路由,其实就是指向的意思,当我点击页面上的home按钮时,页面中就要显示home的内容,如果点击页面上的about 按钮,页面中就要显示about 的内容.Home ...

  6. 经GitHub将kubernetes镜像推送到阿里云

    背景 在安装kubernetes时会出现无法访问镜像站的情况,通过GitHub将kubernetes镜像推送到阿里云之后,即可使用阿里云地址引用所需镜像,现已同步镜像5000+,当前还在陆续同步.仓库 ...

  7. [Windows]解决:windows连接远程桌面-出现身份验证错误,要求的函数不受支持( CredSSP加密数据库修正)[转载]

    文由 需要在本地Windows系统电脑通过远程桌面(mstsc)另一台Windows服务器,将其内的数据拷贝过来.但却发生了这样的异常 解决方案 step1 Win+R step2 打开注册表: gp ...

  8. Java关键字以及标识符

    Java中有许多关键字,关键字是什么意思呢? 我用自己的分析来表达一下吧. Java就是源自于生活的,我们都有自己的名字.所以它也会有许多的名字,每个名字都有各自不同的特性(作用),都是系统定义好的. ...

  9. .Net 6.0 部署Linux+Nginx +PM2教程

    今天带大家将本地.Net6.0项目部署到Linux系统中,其中有用到Nginx反向代理和PM2进程管理工具,希望本偏文章能对你有所帮助,成为你成功路上的垫脚石! 背景: 在.Net 5.0横空出世之后 ...

  10. ts、typescript、enum、枚举、ts 获取枚举对应的类型、获取 enum 的 key 和 value

    ts.typescript.enum.枚举.ts 获取枚举对应的类型 // 假设我一个枚举 enum ENUM_TYPE { ALL = 'all', SOME = 'some', LITTLE = ...