写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

  看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。

 


华丽的分割线


 

NtReadVirtualMemory 分析

  由于是仅仅分析挂靠时该函数是如何备份和恢复APC队列的,为了缩短篇幅增加可读性,我会尽可能使用IDA翻译的伪代码,你的伪代码结果应该和我的不一样,因为我进行了一些重命名操作。我们先定位到NtReadVirtualMemory这个伪代码:

NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
{
_KTHREAD *v5; // edi
PSIZE_T v6; // ebx
int v8; // [esp+10h] [ebp-28h] BYREF
PVOID Object; // [esp+14h] [ebp-24h] BYREF
KPROCESSOR_MODE AccessMode[4]; // [esp+18h] [ebp-20h]
NTSTATUS v11; // [esp+1Ch] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+20h] [ebp-18h] v5 = KeGetCurrentThread();
AccessMode[0] = v5->PreviousMode;
if ( AccessMode[0] )
{
if ( BaseAddress + NumberOfBytesToRead < BaseAddress
|| Buffer + NumberOfBytesToRead < Buffer
|| BaseAddress + NumberOfBytesToRead > MmHighestUserAddress
|| Buffer + NumberOfBytesToRead > MmHighestUserAddress )
{
return 0xC0000005;
}
v6 = NumberOfBytesRead;
if ( NumberOfBytesRead )
{
ms_exc.registration.TryLevel = 0;
if ( NumberOfBytesRead >= MmUserProbeAddress )
*MmUserProbeAddress = 0;
*NumberOfBytesRead = *NumberOfBytesRead;
ms_exc.registration.TryLevel = -1;
}
}
else
{
v6 = NumberOfBytesRead;
}
v8 = 0;
v11 = 0;
if ( NumberOfBytesToRead )
{
v11 = ObReferenceObjectByHandle(ProcessHandle, 0x10u, PsProcessType, AccessMode[0], &Object, 0);
if ( !v11 )
{
v11 = MmCopyVirtualMemory(
Object,
BaseAddress,
v5->ApcState.Process,
Buffer,
NumberOfBytesToRead,
AccessMode[0],
&v8);
ObfDereferenceObject(Object);
}
}
if ( v6 )
{
*v6 = v8;
ms_exc.registration.TryLevel = -1;
}
return v11;
}

  我们可以看到,该函数实现内存拷贝是通过MmCopyVirtualMemory这个函数实现的,我们点击去看看:

NTSTATUS __stdcall MmCopyVirtualMemory(PEX_RUNDOWN_REF RunRef, int a2, PRKPROCESS KPROCESS, volatile void *Address, SIZE_T Length, KPROCESSOR_MODE AccessMode, int a7)
{
struct _KPROCESS *v8; // ebx
PRKPROCESS kprocess; // ecx
NTSTATUS res; // esi
struct _EX_RUNDOWN_REF *RunRefa; // [esp+8h] [ebp+8h] if ( !Length )
return 0;
v8 = RunRef;
kprocess = RunRef;
if ( RunRef == KeGetCurrentThread()->ApcState.Process )
kprocess = KPROCESS;
RunRefa = &kprocess[1].ProfileListHead.Blink;
if ( !ExAcquireRundownProtection(&kprocess[1].ProfileListHead.Blink) )
return STATUS_PROCESS_IS_TERMINATING;
if ( Length <= 0x1FF )
goto LABEL_10;
res = MiDoMappedCopy(v8, a2, KPROCESS, Address, Length, AccessMode, a7);
if ( res == STATUS_WORKING_SET_QUOTA )
{
*a7 = 0;
LABEL_10:
res = MiDoPoolCopy(v8, a2, KPROCESS, Address, Length, AccessMode, a7);
}
ExReleaseRundownProtection(RunRefa);
return res;
}

  你可能看到一个新奇的函数ExAcquireRundownProtection,这个函数是申请一个锁,从网上查阅翻译过来是停运保护(RundownProtection)锁,名字怪怪的听起来怪怪的。

  这个不涉及我们的核心,我们继续分析,发现它内部又是通过MiDoMappedCopy实现进程内存读取的:

NTSTATUS __stdcall MiDoMappedCopy(PRKPROCESS PROCESS, int a2, PRKPROCESS a3, volatile void *Address, SIZE_T Length, KPROCESSOR_MODE AccessMode, int a7)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] v13 = 0;
v22 = a2;
v17 = Address;
v7 = 0xE000;
if ( Length <= 0xE000 )
v7 = Length;
v16 = &MemoryDescriptorList;
Length_1 = Length;
v19 = v7;
v20 = 0;
v14 = 0;
v15 = 0;
while ( Length_1 )
{
if ( Length_1 < v19 )
v19 = Length_1;
KeStackAttachProcess(PROCESS, &ApcState);
BaseAddress = 0;
v12 = 0;
v11 = 0;
ms_exc.registration.TryLevel = 0;
if ( v22 == a2 && AccessMode )
{
v20 = 1;
if ( Length && (a2 + Length < a2 || a2 + Length > MmUserProbeAddress) )
ExRaiseAccessViolation();
v20 = 0;
}
MemoryDescriptorList.Next = 0;
MemoryDescriptorList.Size = 4 * (((v22 & 0xFFF) + v19 + 0xFFF) >> 12) + 28;
MemoryDescriptorList.MdlFlags = 0;
MemoryDescriptorList.StartVa = (v22 & 0xFFFFF000);
MemoryDescriptorList.ByteOffset = v22 & 0xFFF;
MemoryDescriptorList.ByteCount = v19;
MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess);
v12 = 1;
BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0, 0, 0x20u);
if ( !BaseAddress )
{
v13 = 1;
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
}
KeUnstackDetachProcess(&ApcState);
KeStackAttachProcess(a3, &ApcState);
if ( v22 == a2 )
{
if ( AccessMode )
{
v20 = 1;
ProbeForWrite(Address, Length, 1u);
v20 = 0;
}
}
v11 = 1;
qmemcpy(v17, BaseAddress, v19);
ms_exc.registration.TryLevel = -1;
KeUnstackDetachProcess(&ApcState);
MmUnmapLockedPages(BaseAddress, &MemoryDescriptorList);
MmUnlockPages(&MemoryDescriptorList);
Length_1 -= v19;
v22 += v19;
v17 += v19;
}
*a7 = Length;
return STATUS_SUCCESS;
}

  经过分析,发现与APC备份恢复的都是在进程挂靠相关函数上:KeStackAttachProcess和KeUnstackDetachProcess。我们先看看KeStackAttachProcess:

void __stdcall KeStackAttachProcess(PRKPROCESS PROCESS, PRKAPC_STATE ApcState)
{
_KTHREAD *CurrentThread; // esi
char PROCESSa; // [esp+10h] [ebp+8h] CurrentThread = KeGetCurrentThread();
if ( KeGetPcr()->PrcbData.DpcRoutineActive )
KeBugCheckEx(
5u,
PROCESS,
CurrentThread->ApcState.Process,
CurrentThread->ApcStateIndex,
KeGetPcr()->PrcbData.DpcRoutineActive);
if ( CurrentThread->ApcState.Process == PROCESS )
{
ApcState->Process = 1;
}
else
{
PROCESSa = KeRaiseIrqlToDpcLevel();
if ( CurrentThread->ApcStateIndex )
{
KiAttachProcess(CurrentThread, PROCESS, PROCESSa, ApcState);
}
else
{
KiAttachProcess(CurrentThread, PROCESS, PROCESSa, &CurrentThread->SavedApcState);
ApcState->Process = 0;
}
}
}

  重点我们来看看ApcStateIndex,上一篇我们讲过,当正常状态为0,挂靠状态为1.也就是说,他将会走如下代码:

KiAttachProcess(CurrentThread, PROCESS, PROCESSa, ApcState);

  点击去看看里面有啥代码:

void __stdcall KiAttachProcess(_KTHREAD *thread, PRKPROCESS Process, KIRQL irql, PRKAPC_STATE ApcState)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] ++Process->StackCount;
KiMoveApcState(&thread->ApcState, ApcState);
InitializeListHead(thread->ApcState.ApcListHead);
InitializeListHead(&thread->ApcState.ApcListHead[1]);
thread->ApcState.Process = Process;
thread->ApcState.KernelApcInProgress = 0;
thread->ApcState.KernelApcPending = 0;
thread->ApcState.UserApcPending = 0;
if ( ApcState == &thread->SavedApcState )
{
thread->ApcStatePointer[0] = &thread->SavedApcState;
thread->ApcStatePointer[1] = &thread->ApcState;
thread->ApcStateIndex = 1;
}
if ( Process->State )
{
thread->State = 1;
thread->ProcessReadyQueue = 1;
v9 = Process->ReadyListHead.Blink;
thread->WaitListEntry.Flink = &Process->ReadyListHead;
thread->WaitListEntry.Blink = v9;
v9->Flink = &thread->WaitListEntry;
Process->ReadyListHead.Blink = &thread->WaitListEntry;
if ( Process->State == 1 )
{
Process->State = 2;
v10 = KiProcessInSwapListHead;
v11 = &Process->SwapListEntry;
Processa = &Process->SwapListEntry;
ApcStatea = KiProcessInSwapListHead;
do
{
v11->Next = v10;
v12 = v10;
v10 = ApcStatea;
_ECX = &KiProcessInSwapListHead;
_EDX = Processa;
__asm { cmpxchg [ecx], edx }
}
while ( ApcStatea != v12 );
KiSetSwapEvent();
}
thread->WaitIrql = irql;
KiSwapThread();
}
else
{
v4 = &Process->ReadyListHead;
while ( 1 )
{
v8 = v4->Flink;
if ( v4->Flink == v4 )
break;
v5 = v8->Flink;
v6 = v8 - 12;
v7 = v8->Blink;
v7->Flink = v5;
v5->Blink = v7;
BYTE1(v6[37].Flink) = 0;
KiReadyThread(v6);
}
KiSwapProcess(Process, ApcState->Process);
KiUnlockDispatcherDatabase(irql);
}
}

  我们就可以看到里面与APC备份相关操作了:

if ( ApcState == &thread->SavedApcState )
{
thread->ApcStatePointer[0] = &thread->SavedApcState;
thread->ApcStatePointer[1] = &thread->ApcState;
thread->ApcStateIndex = 1;
}

  我们再来看看KeUnstackDetachProcess这个函数:

void __stdcall KeUnstackDetachProcess(PRKAPC_STATE ApcState)
{
PRKAPC_STATE v1; // ebx
_KTHREAD *CurrentThread; // esi
_KPROCESS *CurrentProcess; // edi
int v4; // eax
int v7; // ecx
_KAPC_STATE *v8; // [esp-Ch] [ebp-20h]
int v9; // [esp+4h] [ebp-10h]
int v10; // [esp+Ch] [ebp-8h]
signed __int8 v11; // [esp+13h] [ebp-1h] v1 = ApcState;
if ( ApcState->Process != 1 )
{
CurrentThread = KeGetCurrentThread();
v11 = KeRaiseIrqlToDpcLevel();
if ( !CurrentThread->ApcStateIndex
|| CurrentThread->ApcState.KernelApcInProgress
|| CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->ApcState
|| CurrentThread->ApcState.ApcListHead[1].Flink != &CurrentThread->ApcState.ApcListHead[1] )
{
KeBugCheck(6u);
}
CurrentProcess = CurrentThread->ApcState.Process;
if ( !--CurrentProcess->StackCount && CurrentProcess->ThreadListHead.Flink != &CurrentProcess->ThreadListHead )
{
CurrentProcess->State = 3;
v4 = KiProcessOutSwapListHead;
v10 = KiProcessOutSwapListHead;
do
{
CurrentProcess->SwapListEntry.Next = v4;
v9 = v4;
v4 = v10;
_ECX = &KiProcessOutSwapListHead;
_EDX = &CurrentProcess->SwapListEntry;
__asm { cmpxchg [ecx], edx }
}
while ( v10 != v9 );
KiSetSwapEvent();
v1 = ApcState;
}
v8 = &CurrentThread->ApcState;
if ( v1->Process )
{
KiMoveApcState(v1, v8);
}
else
{
KiMoveApcState(&CurrentThread->SavedApcState, v8);
CurrentThread->SavedApcState.Process = 0;
CurrentThread->ApcStatePointer[0] = &CurrentThread->ApcState;
CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState;
CurrentThread->ApcStateIndex = 0;
}
if ( CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->ApcState )
{
LOBYTE(v7) = 1;
CurrentThread->ApcState.KernelApcPending = 1;
HalRequestSoftwareInterrupt(v7);
}
KiSwapProcess(CurrentThread->ApcState.Process, CurrentProcess);
KiUnlockDispatcherDatabase(v11);
}
}

  我们很快找到了与APC恢复相关的代码:

if ( v1->Process )
{
KiMoveApcState(v1, v8);
}
else
{
KiMoveApcState(&CurrentThread->SavedApcState, v8);
CurrentThread->SavedApcState.Process = 0;
CurrentThread->ApcStatePointer[0] = &CurrentThread->ApcState;
CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState;
CurrentThread->ApcStateIndex = 0;
}

  分析至此,本题就结束了。

QueueUserAPC 引发的血案

  还记着 APC 篇——备用 APC 队列 提供的第一题的参考代码中的一行注释了吗?

DWORD WINAPI ThreadProc(VOID* Param)
{
for (int i =0 ;i<100;i++)
{
SleepEx(1000,TRUE); //思考为什么?
//Sleep(1000);
printf("Running\n");
}
return 0;
}

  为什么我用SleepEx函数而不是用Sleep吗?你思考这个问题了吗?我们来看看下面几个图:

  我们将SleepEx函数用Sleep替换,并注释掉主函数的Sleep看看效果:

  APC正常被执行,接下来我们去掉注释掉主函数的Sleep,继续运行看看:

  这次竟然发现APC没有执行,到底是为什么呢?我们改回原答案,就可以正常执行APC了,也就是我在参考中给的效果图:

  原因将会在本篇后部分进行揭晓。

KAPC

  无论是正常状态还是挂靠状态,都有两个APC队列,一个内核队列,一个用户队列。每当要挂入一个APC函数时,不管是内核APC还是用户APC,内核都要准备一个KAPC的数据结构,并且将这个KAPC结构挂到相应的APC队列中。现在我们看看KAPC的结构:

kd> dt _KAPC
ntdll!_KAPC
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 Spare0 : Uint4B
+0x008 Thread : Ptr32 _KTHREAD
+0x00c ApcListEntry : _LIST_ENTRY
+0x014 KernelRoutine : Ptr32 void
+0x018 RundownRoutine : Ptr32 void
+0x01c NormalRoutine : Ptr32 void
+0x020 NormalContext : Ptr32 Void
+0x024 SystemArgument1 : Ptr32 Void
+0x028 SystemArgument2 : Ptr32 Void
+0x02c ApcStateIndex : Char
+0x02d ApcMode : Char
+0x02e Inserted : UChar

Type

  指明结构体的类型,APC类型为0x12。

Size

  该结构体的大小,值为0x30。

Thread

  指向目标线程的线程结构体的指针,因为任何一个APC都是让目标线程进行完成。

ApcListEntry

  APC队列挂的位置。

KernelRoutine

  指向一个函数,调用ExFreePoolWithTag释放APC。

NormalRoutine

  存储着用户APC总入口或真正的内核APC函数地址,里面具体的细节将会在后面的文章进行介绍。

NormalContext

  当为内核APC,该成员存储着NULL;如果为用户APC,则为真正的APC函数。

SystemArgument1

  APC函数的参数。

SystemArgument2

  APC函数的参数。

ApcStateIndex

  挂哪个队列,有四个值:0、1、2、3,里面的细节将在后面进行介绍。

ApcMode

  指示该APC是内核APC还是用户APC。

Inserted

  表示本APC是否已挂入队列。挂入前值为0,挂入后值为1。

挂入流程

  为了方便理解,我们先撸一下函数大体调用流程:

graph TD
QueueUserAPC--用户层调用--> NtQueueApcThread -.->KeInitializeApc -.->KeInsertQueueApc-.->KiInsertQueueApc

  其中QueueUserAPC这个函数位于kernel32.dll,它会调用内核模块的NtQueueApcThread进行实现,经历过重重调用,使用KeInitializeApc为APC结构体分配内存并进行初始化,调用KeInsertQueueApc进行插入到指定队列,而插入最终由KiInsertQueueApc实现。

KeInitializeApc 函数说明

  为了做好本篇练习,我们先过一下KeInitializeApc的相关说明:

VOID KeInitializeApc
(
IN PKAPC Apc, //KAPC 指针
IN PKTHREAD Thread, //目标线程
IN KAPC_ENVIRONMENT TargetEnvironment, //四种状态
IN PKKERNEL_ROUTINE KernelRoutine, //销毁 KAPC 的函数地址
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine, //用户 APC 总入口或者内核 APC 函数
IN KPROCESSOR_MODE Mode,//要插入用户 APC 队列还是内核 APC 队列
IN PVOID Context//内核APC:NULL,用户APC:真正的APC函数
)

ApcStateIndex 详解

  该成员与KTHREAD + 0x165偏移处的属性同名,但含义不一样。该ApcStateIndex有四个值,如下面表格所示:

值 含义
0 原始环境
1 挂靠环境
2 当前环境
3 插入APC时的当前环境

  前两个值挺好理解,当值为0时,就是指线程的“亲生父母”;如果值为1时,就是指自己的“养父母”。后面的两个值比较绕,下面将会详细解释一下:

  上一篇我们说过,线程在正常情况下ApcStatePointer[0]指向ApcState,ApcStatePointer[1]指向SavedApcState;而在挂靠情况下ApcStatePointer[0]指向SavedApcState,ApcStatePointer[1]指向ApcState。当值为2的时候,插入的是当前进程的队列。什么是当前队列,是我不管你环境是挂靠还是不挂靠,我就插入当前进程的APC队列里面,以初始化APC的时候为基准。还剩下最玄学的一个值,当值为3时,插入的是当前进程的APC队列,此时有修复ApcStateIndex的操作,以插入APC的时候为基准。

KiInsertQueueApc 调用流程

  为了降低本篇思考题难度,我把该函数的调用流程说一下:

  1. 根据KAPC结构中的ApcStateIndex找到对应的APC队列
  2. 再根据KAPC结构中的ApcMode确定是用户队列还是内核队列
  3. 将KAPC挂到对应的队列中(挂到KAPC的ApcListEntry处)
  4. 再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态
  5. 修改KAPC_STATE结构中的KernelApcPending/UserApcPending

Alertable 详解

  Alertable属性位于KTHREAD当中,如下所示:

kd> dt _KTHREAD
ntdll!_KTHREAD
...
+0x164 Alertable : UChar
...

  我们可以发现很多与线程相关的结尾带Ex的函数的参数都会有一个bAlertable,举例如下:

DWORD SleepEx(
DWORD dwMilliseconds, // time-out interval
BOOL bAlertable // early completion option
);
DWORD WaitForSingleObjectEx(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds, // time-out interval
BOOL bAlertable // alertable option
);

  该值指示线程是否运行被APC吵醒,我们开头说QueueUserAPC 引发的血案解决办法就是由该属性捣的鬼。当该属性为0时,当前插入的用户APC函数未必有机会执当UserApcPending = 0时就会无法执行插入的APC,如果Alertable = 1 ,就会使UserApcPending = 1,从而将目标线程唤醒,从等待链表中被摘出来,并挂到调度链表当中执行。

本节练习

本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

  俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成,本篇参考将会在正文给出。

1️⃣ 逆向分析QueueUserAPC完整的调用流程。

2️⃣ 如果在一个无法被唤醒的线程插入一个APC,然后紧接又插入一个,如果设置线程可被唤醒,那么它会执行几个APC呢?请用代码论证。

下一篇

  APC 篇—— APC 执行

APC 篇—— APC 挂入的更多相关文章

  1. APC 篇—— APC 执行

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

  2. APC 篇——备用 APC 队列

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

  3. 羽夏看Win系统内核—— APC 篇

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

  4. APC 篇——初识 APC

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

  5. C++入职学习篇--新员工入职(持续更新)

    C++入职学习篇--新员工入职(持续更新) 本人菜鸟一枚,刚刚结束学业生涯,入职C++软件开发岗位,之前对C++一窍不通,刚刚入职,亚历山大,但为祖国和平发展,本人励志为中华崛起而奋斗,学不好C++誓 ...

  6. 让docker容器使用主机系统时间(挂入/etc/localtime)

    -v挂入这个文件就可以了: -v /etc/localtime:/etc/localtime:ro

  7. APC 篇——总结与提升

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

  8. iOS开发——实用篇Swift篇&QQ登入界面实现

    QQ登入界面实现 我们知道在App Store中几乎所有软件都设计到账户的登入,而我们最常见的就是QQ,微信,在没有踏入程序员这条不归路之前,看到一个个的界面都感觉好高大上的样子. 在学习的过程中,自 ...

  9. [Erlang13]怎么把一个普通的进程挂入Supervisor监控树?

    简单来说:应该是在调用的start_link返回一个{ok,Pid}就可以把这个进程放入监控树Supervisor里面: -module(worker). -author("zhongwen ...

随机推荐

  1. Adversarial Defense by Restricting the Hidden Space of Deep Neural Networks

    目录 概 主要内容 Mustafa A., Khan S., Hayat M., Goecke R., Shen J., Shao L., Adversarial Defense by Restric ...

  2. matplotlib 进阶之Legend guide

    目录 matplotlib.pyplot.legend 方法1自动检测 方法2为现有的Artist添加 方3显示添加图例 控制图例的输入 为一类Artist设置图例 Legend 的位置 loc, b ...

  3. Spring练习,定义三个模块,使用<import>标签完成分模块配置开发,模拟实现学生借书和还书的过程,将结束输出到控制台。

    相关 知识 >>> 相关 练习 >>> 实现要求: 在图书管理系统中,学生管理模块.书籍管理模块和借还书管理模块等其他模块,相互配合协作,促使系统的运行流畅.定义三 ...

  4. Java初学者作业——判断该年份是否为闰年,并输出判断结果

    返回本章节 返回作业目录 需求说明: 从控制台输入年份,判断该年份是否为闰年,并输出判断结果. 实现思路: 可通过运算符实现闰年判断的条件: (1)能被4整除且不能被100整除的年份 实现:(年份 % ...

  5. C#中的记录(record)

    从C#9.0开始,我们有了一个有趣的语法糖:记录(record) 为什么提供记录? 开发过程中,我们往往会创建一些简单的实体,它们仅仅拥有一些简单的属性,可能还有几个简单的方法,比如DTO等等,但是这 ...

  6. MATLAB m文件编译错误之错误使用slCharacterEncoding

    错误提示: 错误使用 slCharacterEncoding (line 51) Close all block diagrams (using 'bdclose all') before tryin ...

  7. centos6.5-搭建LNMP

    安装LNMP 一.安装nginx 1.安装相关组件 yum -y install pcre-devel zlib-devel 2.创建启动用户 useradd -M -s /sbin/nologin ...

  8. JMeter_分布式压测配置

    前置条件 电脑A,电脑B,电脑C均安装相同版本的JMeter 和JDK.其中电脑A作为控制机,电脑B和电脑C作为施压机 电脑A IP:172.16.0.114 电脑B IP:172.16.0.115 ...

  9. Python_上下文管理器

    上下文管理器(context manager)是 Python 编程中的重要概念,用于规定某个对象的使用范围.一旦进入或者离开该使用范围,会有特殊操作被调用 (比如为对象分配或者释放内存).它的语法形 ...

  10. 在CentOS7上安装 jq

    安装EPEL源: yum install epel-release 安装完EPEL源后,可以查看下jq包是否存在: yum list jq 安装jq: yum -y install jq 命令参考资料 ...