将详细分析Windows调试的本机接口。希望读者对C和通用NT内核体系结构和语义有一些基本的了解。此外,这并不是介绍什么是调试或如何编写调试器。它可以作为经验丰富的调试器编写人员或好奇的安全专家的参考。

本机调试

现在是时候看看事情的本机方面,以及ntdll.dll中的包装层如何与内核通信。DbgUi层的优点是它允许更好地分离Win32和NT内核,而NT内核一直是NT设计的一部分。NTDLL和NTOSKRNL是一起构建的,所以他们对彼此有复杂的了解是正常的。它们共享相同的结构,需要有相同的系统调用ID等。在完美的世界中,NT内核应该对Win32一无所知。

此外,它还可以帮助任何希望在本机应用程序中编写调试功能或编写功能齐全的本机模式调试器的人。如果没有DbgUi,就必须手动调用Nt*DebugObject api,并在某些情况下进行大量的前/后处理。DbgUi将所有这些工作简化为一个简单的调用,并提供一个干净的接口来完成。如果内核在内部发生变化,DbgUi可能会保持不变,只会修改其内部代码。

我们从负责创建调试对象并将其与当前进程关联的函数开始探索。与Win32不同,创建调试对象和实际附加到进程之间有着明显的区别。

NTSTATUS
NTAPI
DbgUiConnectToDbg(VOID)
{
OBJECT_ATTRIBUTES ObjectAttributes; /* Don't connect twice */
if (NtCurrentTeb()->DbgSsReserved[]) return STATUS_SUCCESS; /* Setup the Attributes */
InitializeObjectAttributes(&ObjectAttributes, NULL, , NULL, ); /* Create the object */
return ZwCreateDebugObject(&NtCurrentTeb()->DbgSsReserved[],
DEBUG_OBJECT_ALL_ACCESS,
&ObjectAttributes,
TRUE);
}

如您所见,这是一个微不足道的实现,但它向我们展示了两件事。首先,一个线程只能有一个相关联的调试对象,其次,这个对象的句柄存储在TEB的DbgSsReserved数组字段中。回想一下,在Win32中,第一个索引[0]是存储线程数据的位置。我们现在知道了[1]是存放把手的地方。
现在让我们看看如何连接和分离:

NTSTATUS
NTAPI
DbgUiDebugActiveProcess(IN HANDLE Process)
{
NTSTATUS Status; /* Tell the kernel to start debugging */
Status = NtDebugActiveProcess(Process, NtCurrentTeb()->DbgSsReserved[]);
if (NT_SUCCESS(Status))
{
/* Now break-in the process */
Status = DbgUiIssueRemoteBreakin(Process);
if (!NT_SUCCESS(Status))
{
/* We couldn't break-in, cancel debugging */
DbgUiStopDebugging(Process);
}
} /* Return status */
return Status;
} NTSTATUS
NTAPI
DbgUiStopDebugging(IN HANDLE Process)
{
/* Call the kernel to remove the debug object */
return NtRemoveProcessDebug(Process, NtCurrentTeb()->DbgSsReserved[]);
}

同样,这些都是非常简单的实现。但是,我们可以了解到,内核并不负责在远程进程中真正的中断,而是由本机层完成。这个DbgUiIssueRemoteBreakin API在调用DebugBreakProcess时也被Win32使用,所以让我们来看看它:

NTSTATUS
NTAPI
DbgUiIssueRemoteBreakin(IN HANDLE Process)
{
HANDLE hThread;
CLIENT_ID ClientId;
NTSTATUS Status; /* Create the thread that will do the breakin */
Status = RtlCreateUserThread(Process,
NULL,
FALSE,
,
,
PAGE_SIZE,
(PVOID)DbgUiRemoteBreakin,
NULL,
&hThread,
&ClientId); /* Close the handle on success */
if(NT_SUCCESS(Status)) NtClose(hThread); /* Return status */
return Status;
}

它所做的只是在进程内创建一个远程线程,然后返回到调用方。那个远程线程有什么魔力吗?让我们看看:

VOID
NTAPI
DbgUiRemoteBreakin(VOID)
{
/* Make sure a debugger is enabled; if so, breakpoint */
if (NtCurrentPeb()->BeingDebugged) DbgBreakPoint(); /* Exit the thread */
RtlExitUserThread(STATUS_SUCCESS);
}

一点也不特别;线程确保进程真正被调试,然后发出断点。而且,因为这个API是导出的,所以您可以从自己的进程本地调用它来发出调试中断(但请注意,您将杀死自己的线程)。在我们查看Win32调试实现时,我们注意到实际的调试句柄从未使用过,而且调用总是通过DbgUi进行。然后调用NtSetInformationDebugObject系统调用,之前调用了一个特殊的DbgUi API,以实际获取与线程关联的调试对象。这个API也有一个对应的API,所以让我们看看这两个API的作用:

HANDLE
NTAPI
DbgUiGetThreadDebugObject(VOID)
{
/* Just return the handle from the TEB */
return NtCurrentTeb()->DbgSsReserved[];
} VOID
NTAPI
DbgUiSetThreadDebugObject(HANDLE DebugObject)
{
/* Just set the handle in the TEB */
NtCurrentTeb()->DbgSsReserved[] = DebugObject;
}

对于那些熟悉面向对象编程的人来说,这似乎类似于访问器和变异器方法的概念。尽管Win32对这个句柄有完美的访问权限,并且可以自己简单地读取它,NT开发人员还是决定让DbgUi更像一个类,并确保通过这些公共方法访问这个句柄。这种设计允许在必要时将调试句柄存储在其他任何地方,并且只有这两个api需要更改,而不是Win32中的多个dll。
现在访问wait/continue函数,它在Win32下只是包装器:

NTSTATUS
NTAPI
DbgUiContinue(IN PCLIENT_ID ClientId,
IN NTSTATUS ContinueStatus)
{
/* Tell the kernel object to continue */
return ZwDebugContinue(NtCurrentTeb()->DbgSsReserved[],
ClientId,
ContinueStatus);
} NTSTATUS
NTAPI
DbgUiWaitStateChange(OUT PDBGUI_WAIT_STATE_CHANGE DbgUiWaitStateCange,
IN PLARGE_INTEGER TimeOut OPTIONAL)
{
/* Tell the kernel to wait */
return NtWaitForDebugEvent(NtCurrentTeb()->DbgSsReserved[],
TRUE,
TimeOut,
DbgUiWaitStateCange);
}

毫不奇怪,这些函数也是DbgUi中的包装器。然而,这是事情开始变得有趣的地方,因为如果您还记得,DbgUi对调试事件使用完全不同的结构,称为DbgUi-WAIT-STATE-CHANGE。我们还有一个API要看,它负责转换,所以首先,让我们看看这个结构的文档:

//
// User-Mode Debug State Change Structure
//
typedef struct _DBGUI_WAIT_STATE_CHANGE
{
DBG_STATE NewState;
CLIENT_ID AppClientId;
union
{
struct
{
HANDLE HandleToThread;
DBGKM_CREATE_THREAD NewThread;
} CreateThread;
struct
{
HANDLE HandleToProcess;
HANDLE HandleToThread;
DBGKM_CREATE_PROCESS NewProcess;
} CreateProcessInfo;
DBGKM_EXIT_THREAD ExitThread;
DBGKM_EXIT_PROCESS ExitProcess;
DBGKM_EXCEPTION Exception;
DBGKM_LOAD_DLL LoadDll;
DBGKM_UNLOAD_DLL UnloadDll;
} StateInfo;
} DBGUI_WAIT_STATE_CHANGE, *PDBGUI_WAIT_STATE_CHANGE;

这些字段应该很简单,所以让我们看看DBG_STATE枚举:

//
// Debug States
//
typedef enum _DBG_STATE
{
DbgIdle,
DbgReplyPending,
DbgCreateThreadStateChange,
DbgCreateProcessStateChange,
DbgExitThreadStateChange,
DbgExitProcessStateChange,
DbgExceptionStateChange,
DbgBreakpointStateChange,
DbgSingleStepStateChange,
DbgLoadDllStateChange,
DbgUnloadDllStateChange
} DBG_STATE, *PDBG_STATE;

如果您查看Win32调试事件结构和关联的调试事件类型,您会注意到一些可能对您有用的差异。对于初学者,异常、断点和单步异常的处理方式不同。在Win32世界中,只有两个区别:用于异常的RIP_事件和用于调试事件的EXCEPTION_DEBUG_事件。尽管代码稍后可以确定这是一个断点还是一个步骤,但这些信息直接来自本机结构。您还将注意到缺少OUTPUT_DEBUG_STRING事件。在这里,DbgUi处于劣势,因为信息是作为异常发送的,并且需要进行后处理(我们将很快对此进行研究)。Win32还不支持另外两种状态,即空闲状态和应答挂起状态。从调试器的角度来看,它们不提供太多信息,因此被忽略。

现在让我们看看实际的结构:

//
// Debug Message Structures
//
typedef struct _DBGKM_EXCEPTION
{
EXCEPTION_RECORD ExceptionRecord;
ULONG FirstChance;
} DBGKM_EXCEPTION, *PDBGKM_EXCEPTION; typedef struct _DBGKM_CREATE_THREAD
{
ULONG SubSystemKey;
PVOID StartAddress;
} DBGKM_CREATE_THREAD, *PDBGKM_CREATE_THREAD; typedef struct _DBGKM_CREATE_PROCESS
{
ULONG SubSystemKey;
HANDLE FileHandle;
PVOID BaseOfImage;
ULONG DebugInfoFileOffset;
ULONG DebugInfoSize;
DBGKM_CREATE_THREAD InitialThread;
} DBGKM_CREATE_PROCESS, *PDBGKM_CREATE_PROCESS; typedef struct _DBGKM_EXIT_THREAD
{
NTSTATUS ExitStatus;
} DBGKM_EXIT_THREAD, *PDBGKM_EXIT_THREAD; typedef struct _DBGKM_EXIT_PROCESS
{
NTSTATUS ExitStatus;
} DBGKM_EXIT_PROCESS, *PDBGKM_EXIT_PROCESS; typedef struct _DBGKM_LOAD_DLL
{
HANDLE FileHandle;
PVOID BaseOfDll;
ULONG DebugInfoFileOffset;
ULONG DebugInfoSize;
PVOID NamePointer;
} DBGKM_LOAD_DLL, *PDBGKM_LOAD_DLL; typedef struct _DBGKM_UNLOAD_DLL
{
PVOID BaseAddress;
} DBGKM_UNLOAD_DLL, *PDBGKM_UNLOAD_DLL;

如果您熟悉DEBUG_EVENT结构,您应该注意到一些细微的差异。首先,没有进程名的指示,这解释了为什么MSDN将此字段记录为可选字段,而Win32不使用它。您还将注意到线程结构中缺少指向TEB的指针。最后,与新进程不同的是,Win32确实显示了加载的任何新DLL的名称,但在加载DLL结构中似乎也缺少这个名称;我们将很快看到如何处理这个和其他更改。但是,对于额外的信息,我们有“SubsystemKey”字段。由于NT被设计为支持多个子系统,所以这个字段对于识别从哪个子系统创建新线程或进程至关重要。Windows2003SP1增加了对调试POSIX应用程序的支持,虽然我还没有研究过POSIX调试API,但我确信它们是围绕DbgUi实现构建的,而且POSIX库使用此字段的方式不同(很像Win32忽略它)。

现在我们已经看到了这些差异,最后要看的API是DbgUiConvertStateChangeStructure,它负责执行这些修改和修正:

NTSTATUS
NTAPI
DbgUiConvertStateChangeStructure(IN PDBGUI_WAIT_STATE_CHANGE WaitStateChange,
OUT PVOID Win32DebugEvent)
{
NTSTATUS Status;
OBJECT_ATTRIBUTES ObjectAttributes;
THREAD_BASIC_INFORMATION ThreadBasicInfo;
LPDEBUG_EVENT DebugEvent = Win32DebugEvent;
HANDLE ThreadHandle; /* Write common data */
DebugEvent->dwProcessId = (DWORD)WaitStateChange->
AppClientId.UniqueProcess;
DebugEvent->dwThreadId = (DWORD)WaitStateChange->AppClientId.UniqueThread; /* Check what kind of even this is */
switch (WaitStateChange->NewState)
{
/* New thread */
case DbgCreateThreadStateChange: /* Setup Win32 code */
DebugEvent->dwDebugEventCode = CREATE_THREAD_DEBUG_EVENT; /* Copy data over */
DebugEvent->u.CreateThread.hThread =
WaitStateChange->StateInfo.CreateThread.HandleToThread;
DebugEvent->u.CreateThread.lpStartAddress =
WaitStateChange->StateInfo.CreateThread.NewThread.StartAddress; /* Query the TEB */
Status = NtQueryInformationThread(WaitStateChange->StateInfo.
CreateThread.HandleToThread,
ThreadBasicInformation,
&ThreadBasicInfo,
sizeof(ThreadBasicInfo),
NULL);
if (!NT_SUCCESS(Status))
{
/* Failed to get PEB address */
DebugEvent->u.CreateThread.lpThreadLocalBase = NULL;
}
else
{
/* Write PEB Address */
DebugEvent->u.CreateThread.lpThreadLocalBase =
ThreadBasicInfo.TebBaseAddress;
}
break; /* New process */
case DbgCreateProcessStateChange: /* Write Win32 debug code */
DebugEvent->dwDebugEventCode = CREATE_PROCESS_DEBUG_EVENT; /* Copy data over */
DebugEvent->u.CreateProcessInfo.hProcess =
WaitStateChange->StateInfo.CreateProcessInfo.HandleToProcess;
DebugEvent->u.CreateProcessInfo.hThread =
WaitStateChange->StateInfo.CreateProcessInfo.HandleToThread;
DebugEvent->u.CreateProcessInfo.hFile =
WaitStateChange->StateInfo.CreateProcessInfo.NewProcess.
FileHandle;
DebugEvent->u.CreateProcessInfo.lpBaseOfImage =
WaitStateChange->StateInfo.CreateProcessInfo.NewProcess.
BaseOfImage;
DebugEvent->u.CreateProcessInfo.dwDebugInfoFileOffset =
WaitStateChange->StateInfo.CreateProcessInfo.NewProcess.
DebugInfoFileOffset;
DebugEvent->u.CreateProcessInfo.nDebugInfoSize =
WaitStateChange->StateInfo.CreateProcessInfo.NewProcess.
DebugInfoSize;
DebugEvent->u.CreateProcessInfo.lpStartAddress =
WaitStateChange->StateInfo.CreateProcessInfo.NewProcess.
InitialThread.StartAddress; /* Query TEB address */
Status = NtQueryInformationThread(WaitStateChange->StateInfo.
CreateProcessInfo.HandleToThread,
ThreadBasicInformation,
&ThreadBasicInfo,
sizeof(ThreadBasicInfo),
NULL);
if (!NT_SUCCESS(Status))
{
/* Failed to get PEB address */
DebugEvent->u.CreateThread.lpThreadLocalBase = NULL;
}
else
{
/* Write PEB Address */
DebugEvent->u.CreateThread.lpThreadLocalBase =
ThreadBasicInfo.TebBaseAddress;
} /* Clear image name */
DebugEvent->u.CreateProcessInfo.lpImageName = NULL;
DebugEvent->u.CreateProcessInfo.fUnicode = TRUE;
break; /* Thread exited */
case DbgExitThreadStateChange: /* Write the Win32 debug code and the exit status */
DebugEvent->dwDebugEventCode = EXIT_THREAD_DEBUG_EVENT;
DebugEvent->u.ExitThread.dwExitCode =
WaitStateChange->StateInfo.ExitThread.ExitStatus;
break; /* Process exited */
case DbgExitProcessStateChange: /* Write the Win32 debug code and the exit status */
DebugEvent->dwDebugEventCode = EXIT_PROCESS_DEBUG_EVENT;
DebugEvent->u.ExitProcess.dwExitCode =
WaitStateChange->StateInfo.ExitProcess.ExitStatus;
break; /* Any sort of exception */
case DbgExceptionStateChange:
case DbgBreakpointStateChange:
case DbgSingleStepStateChange: /* Check if this was a debug print */
if (WaitStateChange->StateInfo.Exception.ExceptionRecord.
ExceptionCode == DBG_PRINTEXCEPTION_C)
{
/* Set the Win32 code */
DebugEvent->dwDebugEventCode = OUTPUT_DEBUG_STRING_EVENT; /* Copy debug string information */
DebugEvent->u.DebugString.lpDebugStringData =
(PVOID)WaitStateChange->
StateInfo.Exception.ExceptionRecord.
ExceptionInformation[];
DebugEvent->u.DebugString.nDebugStringLength =
WaitStateChange->StateInfo.Exception.ExceptionRecord.
ExceptionInformation[];
DebugEvent->u.DebugString.fUnicode = FALSE;
}
else if (WaitStateChange->StateInfo.Exception.ExceptionRecord.
ExceptionCode == DBG_RIPEXCEPTION)
{
/* Set the Win32 code */
DebugEvent->dwDebugEventCode = RIP_EVENT; /* Set exception information */
DebugEvent->u.RipInfo.dwType =
WaitStateChange->StateInfo.Exception.ExceptionRecord.
ExceptionInformation[];
DebugEvent->u.RipInfo.dwError =
WaitStateChange->StateInfo.Exception.ExceptionRecord.
ExceptionInformation[];
}
else
{
/* Otherwise, this is a debug event, copy info over */
DebugEvent->dwDebugEventCode = EXCEPTION_DEBUG_EVENT;
DebugEvent->u.Exception.ExceptionRecord =
WaitStateChange->StateInfo.Exception.ExceptionRecord;
DebugEvent->u.Exception.dwFirstChance =
WaitStateChange->StateInfo.Exception.FirstChance;
}
break; /* DLL Load */
case DbgLoadDllStateChange : /* Set the Win32 debug code */
DebugEvent->dwDebugEventCode = LOAD_DLL_DEBUG_EVENT; /* Copy the rest of the data */
DebugEvent->u.LoadDll.lpBaseOfDll =
WaitStateChange->StateInfo.LoadDll.BaseOfDll;
DebugEvent->u.LoadDll.hFile =
WaitStateChange->StateInfo.LoadDll.FileHandle;
DebugEvent->u.LoadDll.dwDebugInfoFileOffset =
WaitStateChange->StateInfo.LoadDll.DebugInfoFileOffset;
DebugEvent->u.LoadDll.nDebugInfoSize =
WaitStateChange->StateInfo.LoadDll.DebugInfoSize; /* Open the thread */
InitializeObjectAttributes(&ObjectAttributes, NULL, , NULL, NULL);
Status = NtOpenThread(&ThreadHandle,
THREAD_QUERY_INFORMATION,
&ObjectAttributes,
&WaitStateChange->AppClientId);
if (NT_SUCCESS(Status))
{
/* Query thread information */
Status = NtQueryInformationThread(ThreadHandle,
ThreadBasicInformation,
&ThreadBasicInfo,
sizeof(ThreadBasicInfo),
NULL);
NtClose(ThreadHandle);
} /* Check if we got thread information */
if (NT_SUCCESS(Status))
{
/* Save the image name from the TIB */
DebugEvent->u.LoadDll.lpImageName =
&((PTEB)ThreadBasicInfo.TebBaseAddress)->
Tib.ArbitraryUserPointer;
}
else
{
/* Otherwise, no name */
DebugEvent->u.LoadDll.lpImageName = NULL;
} /* It's Unicode */
DebugEvent->u.LoadDll.fUnicode = TRUE;
break; /* DLL Unload */
case DbgUnloadDllStateChange: /* Set Win32 code and DLL Base */
DebugEvent->dwDebugEventCode = UNLOAD_DLL_DEBUG_EVENT;
DebugEvent->u.UnloadDll.lpBaseOfDll =
WaitStateChange->StateInfo.UnloadDll.BaseAddress;
break; /* Anything else, fail */
default: return STATUS_UNSUCCESSFUL;
} /* Return success */
return STATUS_SUCCESS;
}

让我们看看这些有趣的装饰。首先,通过使用ThreadBasicInformation类型调用NtQueryInformationThread很容易修复TEB指针的不足,该类型返回指向TEB的指针,然后将其保存在Win32结构中。至于调试字符串,API分析异常代码并查找DBG_PRINTEXCEPTION_C,它有一个特定的异常记录,该记录被解析并转换为调试字符串输出。

到目前为止还算不错,但在加载DLL的代码中可能出现了最糟糕的黑客攻击。因为加载的DLL在内核内存中没有EPROCESS或ETHREAD这样的结构,但是在ntdll的私有Ldr结构中,唯一标识它的是内存中内存映射文件的节对象。当内核收到为可执行内存映射文件创建节的请求时,它会将文件名保存在TEB(或者TIB,更确切地说,是TIB)中的一个字段中,该字段称为arbiryuserpointer。

然后,此函数知道一个字符串位于那里,并将其设置为调试事件的lpImageName成员的指针。自从第一次构建以来,这个黑客就一直在NT中,据我所知,它仍然存在于Vista中。会这么难解决吗?

再次,我们的讨论到此结束,因为ntdll中处理调试对象的部分已经所剩无几。以下是本系列本部分讨论内容的概述:

  • DbgUi在内核和Win32或其他子系统之间提供了一定程度的分离。它是作为一个完全独立的类编写的,甚至有访问器和变异器方法,而不是公开它的句柄。
  • 线程调试对象的句柄存储在TEB中DbgSsReserved数组的第二个字段中。
  • DbgUi允许一个线程有一个DebugObject,但是使用本机系统调用允许您做任何您想要做的事情。
  • 大多数dbguiapi是NtXxxDebugObject系统调用的简单包装器,并使用TEB句柄进行通信。
  • DbgUi负责进入附加的进程,而不是内核。
  • DbgUi对调试事件使用自己的结构,内核可以理解这种结构。在某些方面,此结构提供了有关某些事件的更多信息(例如子系统以及这是单步还是断点异常),但在其他方面,缺少某些信息(例如指向线程TEB的指针或单独的调试字符串结构)。
  • TIB(位于TEB的仲裁指针成员中)包含调试事件期间加载的DLL的名称。

Windows本机调试内部组件的更多相关文章

  1. Windows用户模式调试内部组件

    简介 允许用户模式调试工作的内部机制很少得到充分的解释.更糟糕的是,这些机制在Windows XP中已经发生了根本性的变化,当许多支持被重新编写时,还通过将ntdll中的大多数例程作为本地API的一部 ...

  2. C#学习日志 day 5 ------ windows phone 8.1真机调试手机应用

    在vs2013中,可以写windows phone 8.1的程序,但是调试时需要用到windows自带的虚拟机hyper-V 正版的系统开启hyper—V的时候不会有问题,但是盗版的系统可能导致系统不 ...

  3. eros --- Windows Android真机调试

    1.下载并安装JDK 2.下载并安装Android Studio 上面两项不管用weex还是eros都是前置条件,度娘有大量教程. 开始eros 手脚架安装: $ npm i -g eros-cli ...

  4. Windows 下 Hbuilder 真机调试(Android,iphone)

    概述:主要讲讲自己在使用 HBuilder 真机调试功能时遇到的问题,以及如何解决.Android 相对没有遇到什么大问题,在电脑安装如360手机助手就可以正常使用了,主要问题是在 iphone 上( ...

  5. Docker进阶之二:Docker内部组件

    Docker内部组件 一.Namespaces 命名空间,Linux内核提供的一种对进程资源隔离的机制,例如进程,网络,挂载点等资源.    docker run -d busybox ping ba ...

  6. 如何实用便捷的在本地真机调试WEB端HTML5网页

    先简单介绍两款常用但需要一定条件或限制的工具 1.如果你能FQ chrome在32版本后就自带了移动端调度工具,可以在Android直接联调,但唯一遗憾的是,在我大天朝要FQ后才能行的通,我自己试了后 ...

  7. [IOS]从零开始搭建基于Xcode7的IOS开发环境和免开发者帐号真机调试运行第一个IOS程序HelloWorld

    首先这篇文章比较长,若想了解Xcode7的免开发者帐号真机调试运行IOS程序的话,直接转到第五部分. 转载请注明原文地址:http://www.cnblogs.com/litou/p/4843772. ...

  8. xamarin.forms uwp app部署到手机移动设备进行测试,真机调试(device portal方式部署)

    最近学习xamarin.刚好 手上有一个lumia 930.所以试一试把uwp app部署到手机上,并真机调试一把. 目前环境: 1.开发pc电脑是win10,版本1607.加入了insider,所以 ...

  9. 【系统篇】从int 3探索Windows应用程序调试原理

    探索调试器下断点的原理 在Windows上做开发的程序猿们都知道,x86架构处理器有一条特殊的指令——int 3,也就是机器码0xCC,用于调试所用,当程序执行到int 3的时候会中断到调试器,如果程 ...

随机推荐

  1. gRPC-拦截器简单使用

    概述 gRPC作为通用RPC框架,内置了拦截器功能.包括服务器端的拦截器和客户端拦截器,使用上大同小异.主要作用是在rpc调用的前后进行额外处理. 从客户端角度讲,可以在请求发起前,截取到请求参数并修 ...

  2. PV、UV、UIP、VV、DAU、CTR指的是什么?

    PV(page view) 网站浏览量,指网页的浏览次数,用户每打开一次页面就记录一次PV,多次打开则累加. UV(unique vistor) 独立访客数,指的是某一天访问某站点的人数,以cooki ...

  3. (九)pdf的构成之文件体(content属性)

    content属性简单当成一个流来处理 流内部属一个画笔,下面介绍画笔属性 文本对象: BT    文本开始 ET    文本结束   文本状态:       Tc    字符之间的距离       ...

  4. Redis主从架构搭建和哨兵模式(四)

    一主一从,往主节点去写,在从节点去读,可以读到,主从架构就搭建成功了 1.启用复制,部署slave node wget http://downloads.sourceforge.net/tcl/tcl ...

  5. 2019 UCloudjava面试笔试题 (含面试题解析)

      本人5年开发经验.18年年底开始跑路找工作,在互联网寒冬下成功拿到阿里巴巴.今日头条.UCloud等公司offer,岗位是Java后端开发,因为发展原因最终选择去了UCloud,入职一年时间了,也 ...

  6. python-tyoira基本

    目录 .Typora安装 我们在之前的时候记录笔记就是使用word和记事本,但是从今天开始我们要更换软件,记录笔记使用Typora软件,为什么要使用Typora的软件呢,是因为我们程序员不只是写代码这 ...

  7. 遍历js中的数组

    可以使用js中的for循环,或者forEach方法:也可以使用Ext中的方法遍历js中的数组 代码如下: /** * 遍历数组 */ var arr = ['越南', '新加坡', '美国', '俄罗 ...

  8. 纯css更改图片颜色的技巧

    tips: JPG.PNG.GIF 都可以,但是有一个前提要求,就是黑色纯色,背景白色 .pic1 {     background-image: url($img), linear-gradient ...

  9. Android为TV端助力之热修复原理

    通过源码我们知道Android加载类是通过ClassLoad类里面的findClass先去查找的,如下图所示 通过看源码我们知道,ClassLoad是一个抽象类,它本身并没有实现findclass() ...

  10. JS基础 浏览器弹出的三种提示框(提示信息框、确认框、输入文本框)

    浏览器的三种提示框 alert() //提示信息框 confirm() //提示确认框 prompt() //提示输入文本框 1.alert( ) 提示信息框 <script> alert ...