显卡驱动入手的构造D3DHook的方法
本文要点:
介绍一种从显卡驱动入手的构造D3DHook的方法,DX9,DX9EX,DX10,DX11均试用,无需重启进程,注入即可使用。
以DX9为例。
前置知识:
1. API Hook的基础知识
2. DX绘图知识
3. 关于DX Hook 有很多种方法,传统的方法请自行查找资料。
4. Windows Display Driver 的基础知识
首先Vender驱动导出了一个叫做OpenAdapter的函数。
原型
1
|
typedef HRESULT APIENTRY _OpenAdapter(D3DDDIARG_OPENADAPTER
|
*pAdapterData);
通过观察这个函数的参数可以发现这两个结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
typedef struct _D3DDDIARG_OPENADAPTER
{
HANDLE hAdapter; // in /out : Runtime handle /out : Driver handle
UINT Interface; // in : Interface version
UINT Version; // in : Runtime version
CONST D3DDDI_ADAPTERCALLBACKS* pAdapterCallbacks; // in : Pointer to runtime callbacks
D3DDDI_ADAPTERFUNCS* pAdapterFuncs; // out: Driver function table
UINT DriverVersion; // out: D3D UMD interface version the
// driver was compiled with. Use
// D3D_UMD_INTERFACE_VERSION.
} D3DDDIARG_OPENADAPTER;
typedef struct _D3DDDI_ADAPTERFUNCS
{
PFND3DDDI_GETCAPS pfnGetCaps;
PFND3DDDI_CREATEDEVICE pfnCreateDevice;
PFND3DDDI_CLOSEADAPTER pfnCloseAdapter;
} D3DDDI_ADAPTERFUNCS;
|
HookOpenAdapter代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
BOOL DetourOpenAdapter()
{
OpenAdapter = (_OpenAdapter *)GetProcAddress(GetModuleHandleA( "nvd3dum.dll" ), "OpenAdapter" );
if (OpenAdapter == NULL)
{
OpenAdapter = (_OpenAdapter *)GetProcAddress(GetModuleHandleA( "aticfx32.dll" ), "OpenAdapter" );
}
if (OpenAdapter)
{
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach((void **)&OpenAdapter, WarpOpenAdapter);
DetourTransactionCommit();
}
return OpenAdapter != NULL;
}
|
通过Hook OpenAdapter 可以得到pfnCreateDevice;
代码如下
1
2
3
4
5
6
7
8
9
10
|
HRESULT APIENTRY WarpOpenAdapter(D3DDDIARG_OPENADAPTER *pAdapterData)
{
HRESULT ret = OpenAdapter(pAdapterData);
if (ret == S_OK && pAdapterData->pAdapterFuncs->pfnCreateDevice)
{
DdiCreateDevice = pAdapterData->pAdapterFuncs->pfnCreateDevice;
pAdapterData->pAdapterFuncs->pfnCreateDevice = WarpDdiCreateDevice;
}
return ret;
}
|
pfnCreateDevice是Vender驱动上报给runtime的createDevice API.当APP调用CreateDevice最后会到厂商驱动的pfnCreateDevice; 替换厂商驱动上报给runtime的pfnCreateDevice函数指针,就可以实现Hook pfnCreateDevice。这样下次APP调用CreateDevice就会先进入我们的WarpDdiCreateDevice;
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
HRESULT APIENTRY WarpDdiCreateDevice(
HANDLE hAdapter,
D3DDDIARG_CREATEDEVICE *pDeviceData)
{
// DdiCreateDevice must not be NULL if this path hit
HRESULT ret = DdiCreateDevice(hAdapter, pDeviceData);
if (pDeviceData->pDeviceFuncs->pfnPresent && gDetourFuncTable.DdiPresent.isAttatched == FALSE)
{
DdiPresent = pDeviceData->pDeviceFuncs->pfnPresent;
ULONG_PTR realTramponLine = DetourDDiPresent();
gDetourFuncTable.DdiPresent.isAttatched = TRUE;
gDetourFuncTable.DdiPresent.preFuncPtr = DdiPresent;
gDetourFuncTable.DdiPresent.TrampoLinePtr = (PFND3DDDI_PRESENT)realTramponLine;
gDetourFuncTable.DdiPresent.warpFuncPtr = WarpDdiPresent;
}
if (pDeviceData->pDeviceFuncs->pfnPresent1 && gDetourFuncTable.DdiPresent1.isAttatched == FALSE)
{
DdiPresent1 = pDeviceData->pDeviceFuncs->pfnPresent1;
ULONG_PTR realTramponLine = DetourDDiPresent1();
gDetourFuncTable.DdiPresent1.isAttatched = TRUE;
gDetourFuncTable.DdiPresent1.preFuncPtr = DdiPresent1;
gDetourFuncTable.DdiPresent1.TrampoLinePtr = (PFND3DDDI_PRESENT1)realTramponLine;
gDetourFuncTable.DdiPresent1.warpFuncPtr = WarpDdiPresent1;
}
DdiLock = pDeviceData->pDeviceFuncs->pfnLock;
DdiCreateResource = pDeviceData->pDeviceFuncs->pfnCreateResource;
DdiCreateResource2 = pDeviceData->pDeviceFuncs->pfnCreateResource2;
DdiUnlock = pDeviceData->pDeviceFuncs->pfnUnlock;
DdiBlt = pDeviceData->pDeviceFuncs->pfnBlt;
return ret;
}
typedef struct _D3DDDIARG_CREATEDEVICE
{
HANDLE hDevice; // in : Runtime handle /out : Driver handle
UINT Interface; // in : Interface version
UINT Version; // in : Runtime Version
CONST D3DDDI_DEVICECALLBACKS* pCallbacks; // in : Pointer to runtime callbacks
VOID* pCommandBuffer; // in : Pointer to the first command buffer to use.
UINT CommandBufferSize; // in : Size of the first command buffer to use.
D3DDDI_ALLOCATIONLIST* pAllocationList; // out: Pointer to the first allocation list to use.
UINT AllocationListSize; // in : Size of the allocation list that will be available
// when the first command buffer is submitted.
D3DDDI_PATCHLOCATIONLIST* pPatchLocationList; // out: Pointer to the first patch location list to use.
UINT PatchLocationListSize; // in : Size of the patch location list that will be available
// when the first command buffer is submitted.
D3DDDI_DEVICEFUNCS* pDeviceFuncs; // out: Driver function table
D3DDDI_CREATEDEVICEFLAGS Flags; // in : Flags
#if (D3D_UMD_INTERFACE_VERSION >= D3D_UMD_INTERFACE_VERSION_WIN7)
D3DGPU_VIRTUAL_ADDRESS CommandBuffer; // out: GPU Virtual address to the command buffer to use. _ADVSCH_
#endif // D3D_UMD_INTERFACE_VERSION
} D3DDDIARG_CREATEDEVICE;
|
观察参数D3DDDIARG_CREATEDEVICE *pDeviceData不难发现 pDeviceFuncs中有我们需要的一切绘图API,这里用到的函数如下:
1
2
3
4
5
6
7
8
|
PFND3DDDI_CREATEDEVICE DdiCreateDevice = NULL;
PFND3DDDI_PRESENT DdiPresent = NULL;
PFND3DDDI_PRESENT1 DdiPresent1= NULL;
PFND3DDDI_LOCK DdiLock = NULL;
PFND3DDDI_CREATERESOURCE DdiCreateResource = NULL;
PFND3DDDI_CREATERESOURCE2 DdiCreateResource2 = NULL;
PFND3DDDI_UNLOCK DdiUnlock = NULL;
PFND3DDDI_BLT DdiBlt = NULL;
|
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
HRESULT APIENTRY WarpDdiCreateDevice(
HANDLE hAdapter,
D3DDDIARG_CREATEDEVICE *pDeviceData)
{
// DdiCreateDevice must not be NULL if this path hit
HRESULT ret = DdiCreateDevice(hAdapter, pDeviceData);
if (pDeviceData->pDeviceFuncs->pfnPresent && gDetourFuncTable.DdiPresent.isAttatched == FALSE)
{
DdiPresent = pDeviceData->pDeviceFuncs->pfnPresent;
ULONG_PTR realTramponLine = DetourDDiPresent();
gDetourFuncTable.DdiPresent.isAttatched = TRUE;
gDetourFuncTable.DdiPresent.preFuncPtr = DdiPresent;
gDetourFuncTable.DdiPresent.TrampoLinePtr = (PFND3DDDI_PRESENT)realTramponLine;
gDetourFuncTable.DdiPresent.warpFuncPtr = WarpDdiPresent;
}
if (pDeviceData->pDeviceFuncs->pfnPresent1 && gDetourFuncTable.DdiPresent1.isAttatched == FALSE)
{
DdiPresent1 = pDeviceData->pDeviceFuncs->pfnPresent1;
ULONG_PTR realTramponLine = DetourDDiPresent1();
gDetourFuncTable.DdiPresent1.isAttatched = TRUE;
gDetourFuncTable.DdiPresent1.preFuncPtr = DdiPresent1;
gDetourFuncTable.DdiPresent1.TrampoLinePtr = (PFND3DDDI_PRESENT1)realTramponLine;
gDetourFuncTable.DdiPresent1.warpFuncPtr = WarpDdiPresent1;
}
DdiLock = pDeviceData->pDeviceFuncs->pfnLock;
DdiCreateResource = pDeviceData->pDeviceFuncs->pfnCreateResource;
DdiCreateResource2 = pDeviceData->pDeviceFuncs->pfnCreateResource2;
DdiUnlock = pDeviceData->pDeviceFuncs->pfnUnlock;
DdiBlt = pDeviceData->pDeviceFuncs->pfnBlt;
return ret;
}
|
到这里我们已经顺利拿到厂商的pfnPresent函数了,是不是有点兴奋呢?因为我们要做的任何绘图工作需要在pfnPresent之前完成。这样present才会把我们画的东西拷贝到屏幕上。
或许你会有一个疑问,APP已经在运行了,不会再调用CreateDevice了,这样我们的Hook不就无效了吗?
答案是这样的,既然APP调用CreateDevice,那我们就自己调用。我们来构造这样一个CreateDevice的条件,创建出来的d3dDevice还能在后面用来作为我们绘图的设备(OnRenderFrame).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
BOOL TryInitD3D(HWND hWnd)
{
D3DPRESENT_PARAMETERS d3dpp;
HRESULT hResult = D3DERR_INVALIDCALL;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
pd3dInternal = Direct3DCreate9(D3D_SDK_VERSION);
if (pd3dInternal != NULL)
{
hResult = pd3dInternal->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, &pd3dDevInternal);
}
return hResult == S_OK && pd3dDevInternal != NULL && InitDx();
}
|
拿到pfnPresent之后做inlineHook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
ULONG_PTR DetourDDiPresent1()
{
ULONG_PTR realTrampoLine = NULL;
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
if (DdiPresent1)
{
DetourAttachEx((void **)&DdiPresent1, WarpDdiPresent1, (PDETOUR_TRAMPOLINE *)&realTrampoLine, NULL, NULL);
}
DetourTransactionCommit();
return realTrampoLine;
}
|
然后在pfnPrsent的代理函数中做我们自己的绘图操作。里面涉及的函数下面会有解释
我实现了一个简单的pfnPresent1代理函数,在里面调用真正的pfnPresent1之前做我们的绘图(我是的WIN 8.1,WDDM1.3 所以就对pfnPresent1动手了)
代理函数如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
HRESULT APIENTRY WarpDdiPresent1(
HANDLE hDevice,
CONST D3DDDIARG_PRESENT1 *pPresent1)
{
static HANDLE wrapDevice = NULL;
static HANDLE hDestSurf = NULL;
static UINT cachedWidth = 0;
static UINT cachedHeight = 0;
RECT rc = { 0 };
RECT destRC = { 0 };
RECT srcRC = { 0 };
IDirect3DSurface9 * pSrcSurf = NULL;
HRESULT hRet = S_OK;
// 如果是运行中的app画图,就进入下面的流程进行我们的画图
// 反之是我们的画图操作,直接放过,避免重入
if (hDevice == wrapDevice || wrapDevice == NULL)
{
wrapDevice = hDevice;
if (S_OK == onRenderFrame(&pSrcSurf, &rc))
{
if (rc.right - rc.left != cachedWidth ||
rc.bottom - rc. top != cachedHeight)
{
cachedWidth = rc.right - rc.left;
cachedHeight = rc.bottom - rc. top ;
//TODO : destroy hDestSurf
hDestSurf = CreateSurfaceforWDDM1_3(hDevice, cachedWidth, cachedHeight);
}
if (hDestSurf != NULL)
{
destRC = srcRC = rc;
hRet = SurfaceBltInternal(hDevice, hDestSurf, destRC, pSrcSurf, srcRC);
}
if (hRet == S_OK)
{
BltToRTWDDM1_3(hDevice, hDestSurf, pPresent1,rc);
}
}
}
hRet = gDetourFuncTable.DdiPresent1.TrampoLinePtr(hDevice, pPresent1);
return hRet;
}
|
HRESULT APIENTRY onRenderFrame(__out IDirect3DSurface9 ** pOutSurf, RECT * pOutRC)是我们的绘图函数,在这里实现我们要的东西,画到RenderTarget上,然后输出RenderTarget和它的大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
HRESULT APIENTRY onRenderFrame(__out IDirect3DSurface9 ** pOutSurf, RECT * pOutRC)
{
HRESULT hr;
RECT rc;
pd3dDev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_RGBA(0, 0, 0, 0), 1.0f, 0);
if (S_OK == pd3dDev->BeginScene())
{
hr = pd3dDev->SetRenderTarget(NULL, pRTSurf);
// 在这里画你需要的任何东西,没错 就是这么任性。
pd3dDev->EndScene();
}
if (hr == S_OK)
{
*pOutSurf = pRTSurf;
*pOutRC = rc;
}
else
{
*pOutSurf = NULL;
pOutRC-> top = 0;
pOutRC->left = 0;
pOutRC->bottom = 0;
pOutRC->right = 0;
}
return hr;
}
|
当在WarpDdiPresent1得到我们的绘图结果(RenderTarget和大小)之后,我们就能把画好的结果拿出来,然后通过调用厂商提供的pfnBlt函数 贴到present的SrcSurf上。
//用上面拿到的pfnCreateResource2 创建一张surface 用来保存我们的绘图结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
HANDLE CreateSurfaceforWDDM1_3(HANDLE hDevice, UINT width, UINT height)
{
D3DDDIARG_CREATERESOURCE2 createResourceData2;
D3DDDI_SURFACEINFO surfList[1];
memset(surfList, 0, sizeof(surfList));
memset(&createResourceData2, 0, sizeof(createResourceData2));
surfList[0].Height = width;
surfList[0].Width = height;
surfList[0].Depth = 1;
surfList[0].pSysMem = NULL;
surfList[0].SysMemPitch = 0;
surfList[0].SysMemSlicePitch = 0;
createResourceData2.Format = D3DDDIFMT_A8R8G8B8;
createResourceData2.Pool = D3DDDIPOOL_NONLOCALVIDMEM;
createResourceData2.MultisampleType = D3DDDIMULTISAMPLE_NONE;
createResourceData2.MultisampleQuality = 0;
createResourceData2.pSurfList = surfList;
createResourceData2.SurfCount = 1;
createResourceData2.MipLevels = 0;
createResourceData2.Fvf = 0;
createResourceData2.VidPnSourceId = 0;
createResourceData2.RefreshRate.Denominator = 0;
createResourceData2.RefreshRate.Numerator = 0;
// use a particular handle that runtime do not use
createResourceData2.hResource = (HANDLE)0xfff00000;
createResourceData2.Flags.Value = 0;
createResourceData2.Rotation = D3DDDI_ROTATION_IDENTITY;
DdiCreateResource2(hDevice, &createResourceData2);
return createResourceData2.hResource;
}
|
//把OnRenderFrame输出的RenderTarget的内容拷贝到创建的Surface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
HRESULT SurfaceBltInternal(HANDLE hDevice, HANDLE hDestSurf, RECT destSurfRC, IDirect3DSurface9 * pSrcSurf, RECT srcSurfRC)
{
D3DDDIARG_LOCK lockdata;
D3DDDIARG_UNLOCK unlockData;
D3DLOCKED_RECT lockedRC;
HRESULT hr;
if (destSurfRC.right - destSurfRC.left < srcSurfRC.right - srcSurfRC.left ||
destSurfRC.bottom - destSurfRC. top < srcSurfRC.bottom - srcSurfRC. top )
{
hr = S_FALSE;
}
else
{
memset(&lockdata, 0, sizeof(lockdata));
memset(&unlockData, 0, sizeof(unlockData));
lockdata.hResource = hDestSurf;
lockdata.SubResourceIndex = 0;
lockdata.Area.left = 0;
lockdata.Area.right = destSurfRC.right - destSurfRC.left;
lockdata.Area. top = 0;
lockdata.Area.bottom = destSurfRC.bottom - destSurfRC. top ;
hr = DdiLock(hDevice, &lockdata);
if (hr == S_OK)
{
hr = pSrcSurf->LockRect(&lockedRC, &srcSurfRC, NULL);
if (S_OK == hr)
{
CHAR *pDest = (CHAR*)lockdata.pSurfData;
CHAR *pSrc = (CHAR*)lockedRC.pBits;
for (UINT i = 0; i < destSurfRC.bottom - destSurfRC. top ; i++)
{
CopyMemory(pDest, pSrc, lockedRC.Pitch < lockdata.Pitch ? lockedRC.Pitch : lockdata.Pitch);
pDest += lockdata.Pitch;
pSrc += lockedRC.Pitch;
}
}
pSrcSurf->UnlockRect();
memset(&unlockData, 0, sizeof(unlockData));
unlockData.hResource = hDestSurf;
unlockData.SubResourceIndex = 0;
DdiUnlock(hDevice, &unlockData);
}
}
return hr;
}
|
//调用厂商提供的pfnBLT把surafce上保存的图画 搬到 present的SrcSurf中,这样 present的时候 我们画的东西就展现出来了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
HRESULT BltToRTWDDM1_1(HANDLE hDevice, HANDLE hRes, CONST D3DDDIARG_PRESENT *pPresent)
{
D3DDDIARG_BLT bltData;
RECT srcRC;
RECT destRC;
memset(&bltData, 0, sizeof(bltData));
memset(&srcRC, 0, sizeof(srcRC));
memset(&destRC, 0, sizeof(destRC));
srcRC.left = 0;
srcRC. top = 0;
srcRC.right = 200;
srcRC.bottom = 200;
destRC.left = 0;
destRC. top = 0;
destRC.right = 200;
destRC.bottom = 200;
bltData.hDstResource = pPresent->hSrcResource;
bltData.DstSubResourceIndex = pPresent->SrcSubResourceIndex;
bltData.hSrcResource = hRes;
bltData.SrcSubResourceIndex = 0;
bltData.SrcRect = srcRC;
bltData.DstRect = destRC;
return DdiBlt(hDevice, &bltData);
}
|
如有错误之处,还请各位指正。欢迎各位大牛小牛牛牛拍砖了。
显卡驱动入手的构造D3DHook的方法相关推荐
- 安装驱动显卡重启计算机,Win7电脑安装显卡驱动后一直重启的解决方法
显卡驱动就是用来驱动显卡的程序,它是硬件所对应的软件,而有不少用户发现在win7系统中安装了显卡驱动之后,电脑就一直不停的重启,而进入到安装模式将显卡驱动卸载后就正常,这是怎么回事呢,接下来小编就跟大 ...
- tesla p4 linux驱动,Ubuntu 16.04. 装tesla p4 显卡驱动+cuda9.0+docker+nvidia-docker 详细方法,这里是服务器为主...
Ubuntu 16.04. 装tesla p4 显卡驱动+cuda9.0+docker+nvidia-docker 详细方法,这里是服务器为主 这里 说明一下,我也是在网上看的教程,小白一个,通过好几 ...
- amd显卡驱动linux 卸载,AMD显卡驱动安装和卸载的正确方法
不正确的卸载和安装或升级会导致各种问题 比如蓝屏/驱动安装不了/新特性的功能没有用/CCC打不开/游戏出问题.下面是学习啦小编跟大家分享的是AMD显卡驱动安装和卸载的正确方法,欢迎大家来阅读学习. A ...
- Ubuntu 安装ATI显卡驱动12.4失败的解决方法
A卡驱动很不靠谱,前几个版本包括最新的12.4在正常情况下是无法安装成功的. 难道ATI觉得Linux用户都是大神...可以自己给他们的驱动打补丁,所以故意放出会出现错误的驱动. ========== ...
- linux 显卡亮度,在Deepin 15.10系统中安装NVIDIA显卡驱动后设置屏幕亮度的方法
首先需要在Deepin 15.10操作系统中安装NVIDIA显卡驱动,参考在Deepin系统中安装英伟达NVIDIA显卡驱动的方法一文,然后按照下面的两种方法设置屏幕亮度,实测在Deepin 15.1 ...
- 华硕K42JC安装显卡驱动后进不了系统解决方法
K42JC是双显卡,Inter集显和NIVIDA独显,一开始用驱动精灵自动更新后,进入win7,在输入用户名密码后,用停留在登陆界面,那个圆圈一直在转啊转.只能强制关机.再次开机,按F8,进入安全模式 ...
- ubuntu16.04安装NVIDIA显卡驱动和C++开发环境配置方法
sudo apt-get purge nvidia* sudo add-apt-repository ppa:graphics-drivers/ppa sudo apt-get update sudo ...
- Ubuntu Linux 3D桌面完全教程 显卡驱动安装方法 compiz特效介绍
[2011年5月7日更新] Ubuntu Linux 3D桌面完全教程,显卡驱动安装方法,compiz特效介绍,常见问题解答. 本教程的从2008年至今,经历了三个重大版本的修改: 最早是一善鱼编写并 ...
- Ubuntu 11.10 Linux 3D桌面完全教程,显卡驱动安装方法,compiz特效介绍,常见问题解答
学技术怎么能不会用服务器,阿里云服务器现8折优惠,还有更多优惠券限量发放 https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.ht ...
最新文章
- Python 抖音机器人,论如何在抖音上找到漂亮小姐姐?
- 跨平台使用exp/imp进行数据库转移时出现丢失对象的原因
- Mathematica常用命令
- Python实现Adaboost
- 评测任务征集 | 全国知识图谱与语义计算大会(CCKS 2022)
- angular下拉框点击无反应_angular 实现 下拉菜单 的 点击其他区域关闭下拉菜单功能?...
- 【Kafka】kafka报错 UnknownHostException: %HOSTGROUP::host_group_zookeeper%: Temporary failure in name re
- 罗技无线网卡linux,linux(debian)安装USB无线网卡(tp-link TL-WN725N rtl8188eu )
- 事务(进程 ID )与另一个进程已被死锁在 lock 资源上,且该事务已被选作死锁牺牲品。请重新运行该事务...
- 怎么获取求生之路服务器信息失败,新人服务器出现问题 求助求助!!!!!...
- 7 ida pro 网盘_7月上市新车汇总 日产轩逸领衔 自主高端红旗HS7最具潜力
- Linux的WIFI架构,Linux Wireless架构总结
- 代码实现利用inf文件安装硬件驱动
- 马氏距离(Mahalanobis Distance)与欧式距离
- Hopping Rabbit---牛客
- Morsel-Driven Parallelism: 一种NUMA感知的并行Query Execution框架
- Unity shader护盾特效
- easyui treegrid
- 如何锻炼现货白银的心态?
- 阿里云ECS主机部署LAMP环境