HyperPlatform
之前也写过一个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的更多相关文章
- VT 入门番外篇——初识 VT
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...
随机推荐
- Synchronized和Volatile的对比
Synchronized和Volatile是并发中的两大关键字,有相似性和不同点. Synchronized更详细介绍参考https://www.cnblogs.com/spark-cc/p/1706 ...
- [CTF]picoCTF-day1
Lets Warm Up If I told you a word started with 0x70 in hexadecimal, what would it start with in ASCI ...
- python中的强制等待、隐性等待、显性等待
运行结果过程中出现Unable to locate element时,1.先确定元素是否定位有误.2.再确定运行过程中是否等待不到位,可以截图,查看查找时页面的状态. 1.使用强制等待 --辅助 2. ...
- flask快速上手
看完这2个基本可以起步flask了 一学习 https://www.codingdict.com/article/4884 二实战 https://blog.miguelgrinberg.com/po ...
- [Java SE]Java版本特性解读:java.util.Optional [JDK1.8-]
1 概述 本质上,这是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空(null/empty). Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范 ...
- [Java]排序算法>交换排序>【快速排序】(O(N*logN)/不稳定/N较大/无序/仅顺序存储)
1 快速排序 1.1 算法思想 快速排序是由冒泡排序改进而得的. 在冒泡排序过程中,只对相邻的2个记录进行比较:因此,每次交换2个相邻记录时,只能消除1个逆序. 若能通过2个(不相邻)记录的1次交换, ...
- day65:Linux:nginx代理&nginx负载均衡
目录 1.nginx代理 2.nginx代理与配置 3.nginx负载均衡调度多web节点(静态页面) 4.nginx负载均衡调度多应用节点(blog) 5.nginx_proxy + web应用节点 ...
- Looper 源码分析
//可以看到我们的Looper是存放在线程独有的ThreadLocal进行隔离的 //也就是每个线程独有一份Looper static final ThreadLocal<Loope ...
- 文件上传漏洞靶场:upload-labs(附在线地址)
重装系统:CentOS 7.6 密钥对验证,或密码验证,根据自身情况选择,博主这边为了ssh连接方便选用的密码校验. WindTerm登录系统 需提前去云服务器的安全组,开放22端口ssh连接. 更新 ...
- class(类)和构造函数(原型对象)
构造函数和class的关系,还有面向对象和原型对象,其实很多人都会很困惑这些概念,这是第二次总结这些概念了,之前一次,没有class类,其实了解了构造函数,class也就很容易理解了 一. 构造函数和 ...