【Windows】实现窗口子类化(基于远程线程注入)

news2024/11/14 19:25:41

目录

前言

原理解释

完整项目

相关文献


文章出处链接:[https://blog.csdn.net/qq_59075481/article/details/140334106]

前言

众所周知,DLL 注入有多种用途,如热修补、日志记录、子类化等。本文重点介绍使用 DLL 注入对窗口进行子类化。子类化是通过更改窗口过程来重新定义窗口的行为。要更改窗口过程,窗口过程应驻留在创建窗口的进程中。我计划通过注入子类化模块在不了解创建窗口的进程的情况下替换窗口行为。本文涉及三个模块。

  1. GUITestProcess:这是创建窗口的目标进程。称为“GUITestProcess”,DLL(“SubClassModule”)被注入其中。这是一个简单的 Win32 应用程序,它有一个窗口,每当按下鼠标左键时,就会绘制一个圆圈。
  2. SubClassModule:这是一个 DLL,它具有新的窗口过程,可以挂接到被注入者的窗口。
  3. Injector:这是实际将注入物注入被注入者的进程。这是一个简单的控制台应用程序。

原理解释

窗口子类化有两种实现方式:一种是 SetWindowLongPtr 指定 GWLP_WNDPROC 参数,替换窗口过程函数,另外一种是使用 ComCtl32.dll (至少 6.0 版本)导出的 SetWindowSubclass 来设置子类化窗口过程(另外几个函数分别是:GetWindowSubclass、RemoveWindowSubclass 和 DefSubclassProc),后者提供更多扩展特性。本文以最基础的 SetWindowLongPtr 子类化进行讲解。

首先是一个桌面窗口程序实例代码:

// GUITestProcess.cpp : 定义应用程序的入口点。
//

#include "framework.h"
#include "GUITestProcess.h"
#include <windowsx.h>

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;								// current instance
TCHAR szTitle[MAX_LOADSTRING];					// The title bar text
TCHAR szWindowClass[MAX_LOADSTRING];			// the main window class name

// Forward declarations of functions included in this code module:
ATOM				MyRegisterClass(HINSTANCE hInstance);
BOOL				InitInstance(HINSTANCE, int);
LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK	About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPTSTR    lpCmdLine,
	int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);

	// TODO: Place code here.
	MSG msg;
	HACCEL hAccelTable;

	// Initialize global strings
	LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
	LoadString(hInstance, IDC_INJECTEE, szWindowClass, MAX_LOADSTRING);
	MyRegisterClass(hInstance);

	// Perform application initialization:
	if (!InitInstance(hInstance, nCmdShow))
	{
		return FALSE;
	}

	hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_INJECTEE));

	// Main message loop:
	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}



//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
//  COMMENTS:
//
//    This function and its usage are only necessary if you want this code
//    to be compatible with Win32 systems prior to the 'RegisterClassEx'
//    function that was added to Windows 95. It is important to call this function
//    so that the application will get 'well formed' small icons associated
//    with it.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_INJECTEE));
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = MAKEINTRESOURCE(IDC_INJECTEE);
	wcex.lpszClassName = szWindowClass;
	wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

	return RegisterClassEx(&wcex);
}

//
//   FUNCTION: InitInstance(HINSTANCE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	HWND hWnd;

	hInst = hInstance; // Store instance handle in our global variable

	hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

	if (!hWnd)
	{
		return FALSE;
	}

	ShowWindow(hWnd, nCmdShow);
	UpdateWindow(hWnd);

	return TRUE;
}

//
//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND	- process the application menu
//  WM_PAINT	- Paint the main window
//  WM_DESTROY	- post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PAINTSTRUCT ps;
	HDC hdc;

	switch (message)
	{
	case WM_LBUTTONDOWN:
	{
		int x = GET_X_LPARAM(lParam);
		int y = GET_Y_LPARAM(lParam);
		HDC hDC = ::GetDC(hWnd);
		::Ellipse(hDC, x, y, (x + 50), (y + 50));
		break;
	}
	case WM_COMMAND:
		wmId = LOWORD(wParam);
		wmEvent = HIWORD(wParam);
		// Parse the menu selections:
		switch (wmId)
		{
		case IDM_ABOUT:
			DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
			break;
		case IDM_EXIT:
			DestroyWindow(hWnd);
			break;
		default:
			return DefWindowProc(hWnd, message, wParam, lParam);
		}
		break;
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		// TODO: Add any drawing code here...
		EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	UNREFERENCED_PARAMETER(lParam);
	switch (message)
	{
	case WM_INITDIALOG:
		return (INT_PTR)TRUE;

	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
		{
			EndDialog(hDlg, LOWORD(wParam));
			return (INT_PTR)TRUE;
		}
		break;
	}
	return (INT_PTR)FALSE;
}

让我们看一下注入的源代码。众所周知,注入是一个简单的 DLL,当 DllMain 被调用时DLL_PROCESS_ATTACH,我正在进行 Hack。

//
case DLL_PROCESS_ATTACH:
{
   //Find the window of the Injectee using its title
   HWND hwnd = ::FindWindow(NULL,TEXT("GUITestProcess") );
 
   //If the window found, then change it's window proc
   if( hwnd )
   {
    oldWindowProc = ::SetWindowLongPtr( hwnd, GWLP_WNDPROC, (LONG_PTR) HackingWndProc );
   }
   break;
}

新的窗口过程看起来像这样,这个窗口过程将替换原窗口的左键单击和右键单击操作:

LRESULT CALLBACK HackingWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 switch( message ) 
 {
  case WM_LBUTTONDOWN:
   {
    int x = GET_X_LPARAM(lParam);
    int y = GET_Y_LPARAM(lParam);
    HDC hDC = ::GetDC( hWnd );
    //::Rectangle(  hDC,x,y,(x+50),(y+50) );
    DrawText( hDC,x,y);
    break;
   }
  case WM_RBUTTONDOWN:
   {
     int x = GET_X_LPARAM(lParam);
     int y = GET_Y_LPARAM(lParam);
     HDC hDC = ::GetDC( hWnd );
     ::Ellipse( hDC,x,y,(x+50),(y+50));
     break;
   }
  case WM_DESTROY:
   {
    PostQuitMessage(0);
    break;
   }
  default:
   return DefWindowProc(hWnd, message, wParam, lParam);
 }
 return 0;
}

接下来我们看一下 Injector 的源代码。

这段代码很简单,首先我们要知道要注入到目标进程(injectee)中的 DLL(SubClassModule)的名字。

这个DLL名称应该是目标进程知道的。因此,它必须写入目标进程的地址空间中。

  • 使用窗口标题找出 GUITestProcess 的窗口。
  • 接下来使用 创建窗口的进程的句柄 GetWindowThreadProcessId。
  • 使用句柄打开注入者的进程。
  • 通过命令行的参数获取要注入的 DLL 的路径。
  • 然后在被注入者的地址空间中分配内存以写入要注入的 DLL 的名称。
  • 获取 LoadLibrary 函数的地址。这是将在注入对象的地址空间中创建的线程要调用的方法。
  • 然后调用 NtCreateThreadEx 在被注入者的地址空间内创建一个线程,这个线程将调用 LoadLibrary 函数来注入 DLL。
  • 当线程调用 LoadLibrary 方法加载 DLL 时,DllMain 被调用的原因为 DLL_PROCESS_ATTACH。参阅注入的代码以查看 DllMain 调用时会发生什么。
  • 等待远程线程执行完毕后再调用该 VirtualFree 方法,否则当远程线程查找 DLL 的名称时,保存 DLL 名称的地址块的名称已被注入器进程释放,这可能会导致崩溃!

注入器的命令行参数:注入器程序路径   Dll 文件路径。

注入器主要代码: 

#include <iostream>
#include <windows.h>
#include <vector>
#include <tlhelp32.h>
#include <shlwapi.h>

#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "advapi32.lib")

#define INJECTEE_NAME TEXT("GUITestProcess")

BOOL ProcessHasLoadDll(
    DWORD pid,
    const TCHAR* dll
);

BOOL ZwCreateThreadExInjectDll(
    DWORD dwProcessId,
    const wchar_t* pszDllFileName
);

int wmain(int argc, wchar_t* argv[])
{

    SetConsoleTitleW(L"SubClassWindowsInjector v.1.0");
    
    if (argc != 2)
    {
        std::cerr << "错误:参数不合法!" << std::endl;
        std::cin.get();
        return -1;
    }


    const size_t dllpthlen =
        wcslen(argv[1]) * sizeof(wchar_t);

    if (dllpthlen < 1)
    {
        std::cerr << "错误:文件路径错误!" << std::endl;
        std::cin.get();
        return -1;
    }

    wchar_t* dllpth = new wchar_t[dllpthlen];
    wcscpy_s(dllpth, dllpthlen, argv[1]);

    BOOL dllextflag = PathFileExistsW(dllpth);
    if (FALSE == dllextflag)
    {
        std::cerr << "错误:文件不存在或者无法访问!" << std::endl;
        std::cin.get();
        return -1;
    }

    if (PathGetDriveNumberW(dllpth) == -1)
    {
        TCHAR szPath[_MAX_PATH] = { 0 };
        TCHAR szDrive[_MAX_DRIVE] = { 0 };
        TCHAR szDir[_MAX_DIR] = { 0 };
        TCHAR szFname[_MAX_FNAME] = { 0 };
        TCHAR szExt[_MAX_EXT] = { 0 };
        TCHAR CurBinPath[MAX_PATH] = { 0 };

        GetModuleFileNameW(NULL, szPath, sizeof(szPath) / sizeof(TCHAR));
        ZeroMemory(CurBinPath, sizeof(CurBinPath));
        _wsplitpath_s(szPath, szDrive, szDir, szFname, szExt);
        wsprintf(CurBinPath, L"%s%s", szDrive, szDir);
        
        wcscat_s(CurBinPath, dllpth);
        
        dllextflag = PathFileExistsW(CurBinPath);
        if (FALSE == dllextflag)
        {
            std::cerr << "错误:文件不存在或者无法访问!" << std::endl;
            std::cin.get();
            return -1;
        }
        dllpth = CurBinPath;
    }

    DWORD dwProcessID = 0;

    //Find the main window of the Injectee
    HWND hwnd = ::FindWindow(NULL, INJECTEE_NAME);

    //Get the process hanlde of injectee
    GetWindowThreadProcessId(hwnd, &dwProcessID);

    if (dwProcessID == 0) {
        std::cerr << "错误:找不到目标窗口!"<< std::endl;
        return 0;
    }

    if (ProcessHasLoadDll(dwProcessID, dllpth) != NULL)
    {
        std::cerr << "警告:PID 为 " << dwProcessID << " 的进程已经包含目标 DLL。" << std::endl;
        return 1;
    }

    if (ZwCreateThreadExInjectDll(dwProcessID, dllpth) == FALSE)
    {
        std::cerr << "错误:注入 PID 为 " << dwProcessID << " 的进程时失败。" << std::endl;
        std::cerr << "原因:" << GetLastError() << std::endl;
        return 1;
    }


    std::cout << "操作已经完成。" << std::endl;
    std::cin.get();
    return 0;
}


BOOL ProcessHasLoadDll(DWORD pid, const TCHAR* dll) {
    /*
    * 参数为TH32CS_SNAPMODULE 或 TH32CS_SNAPMODULE32时,如果函数失败并返回ERROR_BAD_LENGTH,则重试该函数直至成功
    * 进程创建未初始化完成时,CreateToolhelp32Snapshot会返回error 299,但其它情况下不会
    */
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
    while (INVALID_HANDLE_VALUE == hSnapshot) {
        DWORD dwError = GetLastError();
        if (dwError == ERROR_BAD_LENGTH) {
            hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
            continue;
        }
        else {
            printf("CreateToolhelp32Snapshot failed: %d\ncurrentProcessId: %d \t targetProcessId:%d\n",
                dwError, GetCurrentProcessId(), pid);
            return FALSE;
        }
    }
    MODULEENTRY32W mi{};
    mi.dwSize = sizeof(MODULEENTRY32W); //第一次使用必须初始化成员
    BOOL bRet = Module32FirstW(hSnapshot, &mi);
    while (bRet) {
        // mi.szModule是短路径
        if (wcsstr(dll, mi.szModule) || wcsstr(mi.szModule, dll)) {
            if (hSnapshot != NULL) CloseHandle(hSnapshot);
            return TRUE;
        }
        mi.dwSize = sizeof(MODULEENTRY32W);
        bRet = Module32NextW(hSnapshot, &mi);
    }
    if (hSnapshot != NULL) CloseHandle(hSnapshot);
    return FALSE;
}

BOOL ZwCreateThreadExInjectDll(
    DWORD dwProcessId,
    const wchar_t* pszDllFileName
)
{
    size_t pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);
    // 1.打开目标进程
    HANDLE hProcess = OpenProcess(
        PROCESS_ALL_ACCESS, // 打开权限
        FALSE, // 是否继承
        dwProcessId); // 进程 PID
    if (hProcess == nullptr)
    {
        printf("错误:打开目标进程失败!\n");
        return FALSE;
    }
    // 2.在目标进程中申请空间
    LPVOID lpPathAddr = VirtualAllocEx(
        hProcess, 0, pathSize, 
        MEM_RESERVE | MEM_COMMIT, 
        PAGE_READWRITE); 
    if (lpPathAddr == nullptr)
    {
        printf("错误:在目标进程中申请空间失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 3.在目标进程中写入Dll路径
    if (FALSE == WriteProcessMemory(
        hProcess, lpPathAddr, 
        pszDllFileName, pathSize, 
        NULL)) // 实际写入大小
    {
        printf("错误:目标进程中写入Dll路径失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    //4.加载ntdll.dll
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    if (hNtdll == nullptr)
    {
        printf("错误:加载ntdll.dll失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    //5.获取LoadLibraryA的函数地址
    //FARPROC可以自适应32位与64位
    FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandle(L"Kernel32.dll"),
        "LoadLibraryW");
    if (pFuncProcAddr == nullptr)
    {
        printf("错误:获取LoadLibrary函数地址失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    //6.获取ZwCreateThreadEx函数地址,该函数在32位与64位下原型不同
    //_WIN64用来判断编译环境 ,_WIN32用来判断是否是Windows系统
#ifdef _WIN64
    typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown
        );
#else
    typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown
        );
#endif 
    typedef_ZwCreateThreadEx ZwCreateThreadEx =
        (typedef_ZwCreateThreadEx)GetProcAddress(hNtdll, "ZwCreateThreadEx");
    if (ZwCreateThreadEx == nullptr)
    {
        printf("错误:获取ZwCreateThreadEx函数地址失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    //7.在目标进程中创建远线程
    HANDLE hRemoteThread = NULL;
    DWORD lpExitCode = 0;
    DWORD dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
        hProcess,
        (LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr, 0, 0, 0, 0, NULL);
    if (hRemoteThread == nullptr)
    {
        printf("错误:目标进程中创建线程失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    // 8.等待线程结束
    WaitForSingleObject(hRemoteThread, -1);
    GetExitCodeThread(hRemoteThread, &lpExitCode);
    if (lpExitCode == 0)
    {
        printf("错误:目标进程中注入 DLL 失败,请检查提供的 DLL 是否有效!\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 9.清理环境
    VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
    CloseHandle(hRemoteThread);
    CloseHandle(hProcess);
    FreeLibrary(hNtdll);
    return TRUE;
}

子类化效果:

子类化效果

完整项目

你可以从这里获取完整的项目代码:

链接:https://pan.baidu.com/s/1Q3og8dgv7lyzXsrGqZy4kg?pwd=6666 
提取码:6666 

相关文献

  • https://www.cnblogs.com/1yzq/p/12939791.html
  • https://learn.microsoft.com/zh-cn/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass
  • https://stackoverflow.com/questions/65695710/how-to-subclass-another-application-control-using-dll-hook-in-win32
  • https://www.purebasic.fr/english/viewtopic.php?t=81714

文章出处链接:[https://blog.csdn.net/qq_59075481/article/details/140334106]。 

本文发布于:2024.07.10,修改于:2024.07.10。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1915752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

技术文件国产化准备

技术文档的本地化涉及调整内容以满足特定目标市场的文化、语言和技术要求。这一过程超越了简单的翻译&#xff0c;确保文件在文化上适合预期受众&#xff0c;在技术上准确无误。适当的准备对于成功的本地化至关重要&#xff0c;以下步骤概述了一种全面的方法。 分析目标受众 …

022-GeoGebra中级篇-几何对象之直线与坐标轴

本文主要介绍一下GeoGebra中直线的常见输入方式&#xff0c;比如工具栏输入、表达式输入、函数输入&#xff0c;最后再把坐标轴的调用简单介绍一下。内容比起传统的教学更偏向于实战一些&#xff0c;若感兴趣欢迎继续阅读。 目录 一、直线1. 关于工具栏绘制&#xff08;1&#…

Dify中的工具

Dify中的工具分为内置工具&#xff08;硬编码&#xff09;和第三方工具&#xff08;OpenAPI Swagger/ChatGPT Plugin&#xff09;。工具可被Workflow&#xff08;工作流&#xff09;和Agent使用&#xff0c;当然Workflow也可被发布为工具&#xff0c;这样Workflow&#xff08;工…

macOS系统下载navicat安装包

链接: https://pan.baidu.com/s/1SqTIXNL-B8ZMJxIBu1DfIw?pwdc1z8 提取码: c1z8 安装后效果

【Android组件】封装加载弹框

&#x1f4d6;封装加载弹框 ✅1. 构造LoadingDialog✅2. 调用LoadingDialog 效果&#xff1a; ✅1. 构造LoadingDialog 构造LoadingDialog类涉及到设计模式中的建造者模式&#xff0c;进行链式调用&#xff0c;注重的是构建的过程&#xff0c;设置需要的属性。 步骤一&#x…

流模型flow

流模型 Flow 超详解&#xff0c;基于 Flow 的生成式模型&#xff0c;从思路到基础到公式推导到模型理解与应用&#xff08;Flow-based Generative Model&#xff09;_generative flows-CSDN博客

做一个专业的声音分析系统,需要对声音那些评判标准进行计算

为了构建一个专业的声音分析系统&#xff0c;需要对以下评判标准进行计算。每个标准需要相应的算法和技术指标来实现。下面是一些关键的评判标准和如何计算这些标准的具体方法&#xff1a; 1. 音质 清晰度 信噪比&#xff08;SNR&#xff09;&#xff1a;计算音频信号中的信…

Linux udp编程

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

Milvus 核心概念(1) ---- 数据一致性的等级及使用场景

目录 背景 Milvus的数据一致性 设置数据一致性等级 等级类型 PACELC定理 level 详细解释 Strong Bounded staleness Session Eventually 总结 背景 分布式上的可扩展性是个比较重要的concept。Chroma 核心之前写过了,他的最大优势在于轻量级且好用。Milvus相对Ch…

C++ //练习 14.44 编写一个简单的桌面计算器使其能处理二元运算。

C Primer&#xff08;第5版&#xff09; 练习 14.44 练习 14.44 编写一个简单的桌面计算器使其能处理二元运算。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /**********************************************************…

轻松理解c++17的string_view

文章目录 轻松理解c17的string_view设计初衷常见用法构造 std::string_view常用操作作为函数参数 注意事项总结 轻松理解c17的string_view std::string_view 是 C17 引入的一个轻量级、不拥有&#xff08;non-owning&#xff09;的字符串视图类。它的设计初衷是提供一种高效、…

今天小编强烈推荐几款国产APP!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 今天小编强烈推荐几款国产APP,算得上是国产之光。如果能帮助到大家&#xff0c;别忘了给小编点点赞加关注哟&#xff01;更多精彩还在后面。 一、…

第二证券:转股溢价率是什么意思?高好还是低好?

转股溢价率是指可转债在商场上的交易价格相对于其转股价值的溢价份额&#xff0c;能够用来衡量投资者为取得将债券转换为股票权力而付出的额定金额&#xff0c;是可转债的重要指标。 转股溢价率的核算公式为&#xff1a;溢价率&#xff1d;&#xff08;转债价格-转股价值&…

C++ 重载运算符 addition (+), subtraction (-) and multiplication (*)

C 重载运算符 addition , subtraction - and multiplication * 1. Operator Overloading (运算符重载)2. Developing an Operator Overloading Example2.1. Adding an Addition Operator (添加加法运算符)2.2. Overloading Restrictions (重载限制)2.3. 重载运算符 - 和 * Refe…

数字统计

import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别// 注意 while 处理多个 caseint a in.nextInt();i…

拯救中国足球,要不尝试一下DDD事件风暴?

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 张逸老师写了新文章《领域建模的常见问题及解决方案》&#xff0c;我来谈一谈对这篇文章的感想。 &#xff08;1&#xff09;文章一开始&#xff0c;张逸老师大大地赞扬了事件风暴&am…

Linux系统中磁盘管理LVM与挂载

Linux系统中磁盘管理LVM与挂载 本文以属于Linux系统基本概念&#xff0c;如果以查找教程教程&#xff0c;解决问题为主&#xff0c;只需要查看本文后半部分。如需要系统性学习请查看本文前半部分。 本文操作极容易导致主机无法自动重启&#xff0c;请慎重操作。操作前务必要进…

JlmPack在军民两用工业软件生态平台的使用

随着信息技术的飞速发展&#xff0c;军民两用工业软件生态平台在国防建设和民用产业中的应用日益广泛。JlmPack作为一款拥有完全自主加权概率模型下的编码算法发明专利的纯国产压缩工具库&#xff0c;其高效、安全、可控的特性在军民两用工业软件生态平台中拥有巨大的应用潜力。…

闲话银行家舍入法,以及在程序中如何实现

前言 相信对于四舍五入的舍入法&#xff0c;大家都耳熟能详&#xff0c;但对于银行家舍入法&#xff0c;可能就会比较少接触了&#xff01; 可是在金融界&#xff0c;银行家舍入法可是大名鼎鼎的主角之一&#xff0c;主要应用于金融领域和涉及货币计算的场合。 那么&#xf…

Clion中怎么切换不同的程序运行

如下图&#xff0c;比如这个文件夹下面有那么多的项目&#xff1a; 那么我想切换不同的项目运行怎么办呢&#xff1f;如果想通过下图的Edit Configurations来设置是不行的&#xff1a; 解决办法&#xff1a; 如下图&#xff0c;选中项目的CMakeLists.txt&#xff0c;右键再点击…