Windows和Linux动态注入

news2024/12/23 18:04:19

  摘要:最近对动态注入有一些兴趣因此搜索了些资料,简单整理了下相关的技术实现。本文只能够带你理解何如注入以及大概如何实现,对注入的方法描述的并不详细。
  关键字:dll注入,hook,提权
  读者须知:读者需要对Windows和Linux dll加载的基本流程比较熟悉。

  注入就是将自己的代码注入到目标进程中强制目标进程执行,而动态注入就是将动态库强制加载进目标进程的进程空间从而对目标进程进行修改。动态注入有利有弊,可以用于反病毒、反外挂也可以用于投毒或者制作外挂。

1 DLL注入

  DLL是windows平台的动态库,一些Windows的基础功能都是以DLL的形式提供的比如Kernel32.dll、GDI32.dll等。开发者将开发的功能按照模块拆分为不同的动态库,再程序需要使用时再将对应的动态库加载到进程的进程地址空间。而DLL注入就是将一个DLL二进制文件注入加载到目标进程中,强制目标进程执行对应DLL的代码的过程。
  DLL注入的实现方式比较多,比如消息Hook注入,输入法注入,远程线程注入,APC注入,EIP注入,注册表注入等,这里简单描述几种。

1.1 Hook注入

  Windows应用基于消息机制,每个UI程序都有自己的消息队列以及对应的消息处理函数。Windows上可以利用系统提供的Hook API来截获对应的消息。
  SetWindowsHookEx可以用来注册某个消息的回调函数。如果应用触发了注册的消息类型就会调用用户指定的回调函数从而劫持消息,在这里就可以对数据进行二次加工或者中间人攻击。API的详情见MSDN-SetWindowsHookEX。
  进程与进程之间是互相独立隔离的,因此我们需要将Hook的代码写在DLL中,通过将该DLL加载到目标进程的进程空间来进行消息劫持。需要注意的是,当我们安装了全局钩子后,只要进程发出钩子可以劫持的消息,操作系统就会将DLL加载到目标进程中,也就是说实际的装载任务不需要我们来做,我们只要注册全局钩子即可。

全局Hook注入实验
  首先,我们需要完成注册全局钩子和注销的相关实现。具体实现比较简单就是调用对应API进行钩子注册和注销以及输出一些简单的帮助信息方便查看当前进程信息,这里不对具体的API进行描述,需要了解的建议直接看MSDN,非常详细。需要注意的是在处理Hook的消息时不要重复触发已经Hook的消息,这样就会出现无限递归,除非电脑注销否则关不掉进程。

//dllmain.cpp
HMODULE gDllModule = nullptr;
BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved){
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
	{
		gDllModule = hModule;
	}break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

//myhook.h
#pragma once
#include <windows.h>
extern "C" {
	_declspec(dllexport) BOOL registerHook();
	_declspec(dllexport) BOOL unregisterHook();
	_declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
}

//myhook.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "myhook.h"
#include <cstdio>

extern HMODULE gDllModule;
#pragma data_seg("hook_data")
HHOOK gHookHandle = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:hook_data,RWS")

bool hooked = false;
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
	FILE *fp = fopen("G://1.txt", "a+");
	char buff[1024]{};
	char    processFullName[_MAX_PATH] = { 0 };
	DWORD dwpid = GetCurrentProcessId();
	GetModuleFileNameA(NULL, processFullName, _MAX_PATH);

	sprintf(buff, "process %s warning hook is coming %d code is %d WParam is %d, LParam is %d\n", processFullName, hooked, code, wParam, lParam);
	fwrite(buff, strlen(buff), 1, fp);
	fclose(fp);
	printf("%s", buff);
	if (!hooked) {
		//MessageBox(NULL, L"warning hook is coming", L"hook", 0);
		hooked = true;
	}

	return ::CallNextHookEx(gHookHandle, code, wParam, lParam);
}

BOOL registerHook() {
	gHookHandle = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, gDllModule, 0);
	return !!gHookHandle;
}

BOOL unregisterHook() {
	gHookHandle ? UnhookWindowsHookEx(gHookHandle) : TRUE;
	return TRUE;
}

  然后我们只需要手动将DLL加载起来运行注册钩子的函数即可。

#include <iostream>
#include <windows.h>

typedef BOOL(*registerHook)();
typedef BOOL(*unregisterHook)();

int main(){
	HMODULE hmodule = LoadLibraryW(TEXT("E:\\code\\tmp\\dll\\dllhook\\Debug\\dllhook.dll"));
	if (!hmodule) {
		printf("can not load library, error code is %d\n", GetLastError());
		exit(1);
	}

	registerHook rh = (registerHook)GetProcAddress(hmodule, "registerHook");
	if (!rh) {
		printf("can not get the register hook address, error code is %d\n", GetLastError());
		exit(1);
	}

	unregisterHook uh = (unregisterHook)GetProcAddress(hmodule, "unregisterHook");
	if (!uh) {
		printf("can not get the unregister hook address, error code is %d\n", GetLastError());
		exit(1);
	}

	printf("load and get process address succ\n");

	auto ret = rh();
	if (!ret) {
		printf("register hook failed\n");
		exit(1);
	}

	MessageBox(NULL, L"warning hook is coming", L"hook", 0);
	while (true) {
	}
	ret = uh();
	if (!ret) {
		printf("unregister hook failed\n");
		exit(1);
	}

	printf("process running succ\n");
	return 0;
}

  下面是Hook成功的一部分日志,可以看到VS和Everthing的消息都劫持到了。

process D:\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe warning hook is coming 1 code is 0 WParam is 1, LParam is 11530596
process D:\Everything\Everything.exe warning hook is coming 1 code is 0 WParam is 0, LParam is 13630404
process D:\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe warning hook is coming 1 code is 0 WParam is 1, LParam is 11531188

  我们看下Everything进程空间加载的DLL,可以看到我们的DLL已经在进程中了。
在这里插入图片描述

1.2 Remote Thread注入

  Remote Thread就是通过创建远程线程,通过该线程对需要注入的DLL在目标进程空间中加载。
  实现的步骤比较简单,我们需要准备三个产物,需要注入的DLL,注入程序,被注入的Rookie。被注入的DLL不用说了自己想怎么写都行,重点是注入程序,注入的过程和Hook不同,这个需要我们自己主动Load,具体过程如下:

  1. 根据目标进程的名称找到进程ID,如果知道进程ID忽略这一步即可;
  2. 打开目标进程,在目标进程上申请一块内存;
  3. 获取目标进程空间中的LoadLibraryW的函数地址;
  4. 创建远程线程调用LoadLibraryW加载需要注入的DLL;
  5. 打扫现场。

  过程比较简单,但是重点是得有目标进程的权限。
  肉鸡进程和注入程序(就是在Attach时显示了一个框)就不展示了,下面是注入的代码:

#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
//生成系统进程快照,遍历快照根据名字查找对应进程的ID
DWORD fetchProcessID(LPCTSTR processName) {
	HANDLE handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (handle == INVALID_HANDLE_VALUE) {
		printf("can not get the snapshot of process\n"); exit(1);
	}

	PROCESSENTRY32 pe{};
	pe.dwSize = sizeof(pe);
	if (!Process32First(handle, &pe)) {
		printf("can not fetch the process information\n"); exit(1);
	}

	DWORD ret{};
	do {
		if (!lstrcmp(pe.szExeFile, processName)) {
			ret = pe.th32ProcessID; break;
		}
	} while (Process32Next(handle, &pe));
	CloseHandle(handle);
	return ret;
}
//远程线程注入目标DLL
BOOL createRemoteThreadAndInject(DWORD pid, LPCWSTR dllName) {
	HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (!processHandle) {
		printf("can not open process %d\n", pid); exit(1);
	}

	DWORD sz = (_tcslen(dllName) + 1) * sizeof(TCHAR);
	LPVOID palloc = VirtualAllocEx(processHandle, NULL, sz, MEM_COMMIT, PAGE_READWRITE);
	if (!palloc) {
		printf("can not allocate memory on process %d\n", pid); exit(1);
	}

	if (!WriteProcessMemory(processHandle, palloc, dllName, sz, NULL)) {
		printf("can not write memory on process %d\n", pid); exit(1);
	}

	HANDLE pfunc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
	if (!pfunc) {
		printf("can not fetch LoadLibraryW address on process %d\n", pid); exit(1);
	}

	LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pfunc;
	HANDLE  hThread = ::CreateRemoteThread(processHandle, NULL, 0, addr, palloc, 0, NULL);
	if (!hThread){
		printf("CreateRemoteThread - Error!");
		return FALSE; 
	}
	DWORD DllAddr = 0;
	WaitForSingleObject(hThread, -1);
	GetExitCodeThread(hThread, &DllAddr);
	VirtualFreeEx(processHandle, palloc, sz, MEM_DECOMMIT);
	::CloseHandle(processHandle);
	return TRUE;
}

int main(){
	DWORD pid = fetchProcessID(L"remote_hook_rookie.exe");
	createRemoteThreadAndInject(pid, L"E:\\code\\tmp\\dll\\remote_thread_hook\\Debug\\remote_thread_hook.dll");
}

1.3 突破Session 0远程线程注入

Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用,RING3谁都能用。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。

ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。

  Windows的服务和应用程序运行与Session之上,Windows内核6.0之前服务运行于第一个启动运行的Session 0上,该用户的应用程序也运行在Session 0上,后续的用户的应用程序分别运行于Seccion 2,Session 3,…,Session n上。由于有些服务会提权运行,将应用程序和服务运行于相同的Session有安全风险。因此Windos Vista之后只有服务可以托管到Session 0上,应用程序都托管到后续的Session,这样可以将应用程序和服务隔离提高安全性。因此使用CreateRemoteThread进行远程线程注入的时候会遇到权限问题。
  可以通过一些底层的一些API对当前进程进行提权,然后进行注入,如下:

// session_0_inject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <Windows.h>
#include <stdio.h>
#include <iostream>

#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

void ShowError(const char* pszText){
	char szError[MAX_PATH] = { 0 };
	::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
	::MessageBox(NULL, szError, "ERROR", MB_OK);
}

// 提权函数
BOOL EnableDebugPrivilege(){
	HANDLE hToken;
	BOOL fOk = FALSE;
	if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)){
		TOKEN_PRIVILEGES tp;
		tp.PrivilegeCount = 1;
		LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);

		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
		AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);

		fOk = (GetLastError() == ERROR_SUCCESS);
		CloseHandle(hToken);
	}

	return fOk;
}


// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD PID, const char* pszDllFileName){
	HANDLE hProcess = NULL;
	SIZE_T dwSize = 0;
	LPVOID pDllAddr = NULL;
	FARPROC pFuncProcAddr = NULL;
	HANDLE hRemoteThread = NULL;
	DWORD dwStatus = 0;

	EnableDebugPrivilege();

	// 打开注入进程,获取进程句柄
	hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
	if (!hProcess){
		printf("OpenProcess - Error!\n\n");
		return -1;
	}
	// 在注入的进程申请内存地址

	dwSize = lstrlen(pszDllFileName) + 1;
	pDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (!pDllAddr){
		ShowError("VirtualAllocEx - Error!\n\n");
		return FALSE;
	}
	//写入内存地址

	if (!WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory - Error!\n\n");
		return FALSE;
	}
	//加载ntdll
	HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
	if (!hNtdllDll){
		ShowError("LoadLirbary");
		return FALSE;
	}
	// 获取LoadLibraryA函数地址
	pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
	if (!pFuncProcAddr){
		ShowError("GetProcAddress_LoadLibraryA - Error!\n\n");
		return FALSE;
	}

	//获取ZwCreateThreadEx函数地址
	typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
	if (!ZwCreateThreadEx){
		ShowError("GetProcAddress_ZwCreateThread - Error!\n\n");
		return FALSE;
	}
	// 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
	dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
	if (!ZwCreateThreadEx){
		ShowError("ZwCreateThreadEx - Error!\n\n");
		return FALSE;
	}
	// 关闭句柄
	::CloseHandle(hProcess);
	::FreeLibrary(hNtdllDll);

	return TRUE;
}

int main(int argc, char* argv[]){
#ifdef _WIN64
	BOOL bRet = ZwCreateThreadExInjectDll(4924, "E:\code\tmp\dll\remote_thread_hook\Debug\remote_thread_hook.dll");
#else 
	BOOL bRet = ZwCreateThreadExInjectDll(15596, "E:\\code\\tmp\\dll\\remote_thread_hook\\Debug\\remote_thread_hook.dll");
#endif
	if (!bRet){
		printf("Inject Dll Error!\n\n");
	}else {
		printf("Inject Dll OK!\n\n");
	}
	return 0;
}

2 Linux 动态注入

  Linux下的动态注入相对比较简单,有两种方式:

  1. 符号表劫持。Linux上ld记载动态库之前会先加载LD_PRELOAD定义的动态库,而符号解析是按照库的载入顺序来的,也就是现在入的库的符号会覆盖后载入的库的符号,利用这种方式我们就可以劫持符号,但是意义不大;
  2. 利用gdb或者lldb手动重定位期望劫持的API的地址。

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

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

相关文章

hadoop -- Yarn资源管理

Yarn YARN被设计用以解决以往架构的需求和缺陷的资源管理和调度软件。 Apache Hadoop YARN &#xff08;Yet Another Resource Negotiator&#xff0c;另一种资源协调者&#xff09;是一种新的 Hadoop 资源管理器&#xff0c;它是一个通用资源管理系统和调度平台&#xff0c;可…

基于uprobe的调试调优浅析

uprobe与krobe对应&#xff0c;动态附加到用户态调用函数的切入点称为uprobe&#xff0c;相比如kprobe 内核函数的稳定性&#xff0c;uprobe 的函数由开发者定义。uprobe是用户态的探针&#xff0c;它和kprobe是相对应的&#xff0c;kprobe是内核态的探针。uprobe需要制定用户态…

深度学习模型压缩方法综述

深度学习因其计算复杂度或参数冗余,在一些场景和设备上限制了相应的模型部署,需要借助模型压缩、系统优化加速等方法突破瓶颈,本文主要介绍模型压缩的各种方法,希望对大家有帮助。 1,模型压缩技术概述 我们知道,一定程度上,网络越深,参数越多,模型也会越复杂,但其最终…

基于Smb协议实现网络文件传输

文章目录 什么是SMB协议SMB与CIFS区别为什么要使用SMB如何对接SMB服务如何用Java实现Smb文件传输SmbV1的实现基于SmbV1的文件上传基于SmbV1的文件下载基于SmbV1的文件重命名基于SmbV1的文件删除基于SmbV1的文件查询 SmbV2的实现基于SmbV2的文件上传基于SmbV2的文件下载基于SmbV…

面向开发人员的 ChatGPT 提示词教程中文版 - ChatGPT 版

面向开发人员的 ChatGPT 提示词教程中文版 - ChatGPT 版 1. 指南1-1. 提示的指南1-2. 配置1-3. 提示语原则原则 1: 写出清晰而具体的指示技巧 1: 使用分隔符来清楚地表明输入的不同部分技巧 2: 要求提供结构化的输出技巧 3: 要求模型检查条件是否得到满足技巧 4: "少许样本…

QML Canvas 元素(绘制文本)

关于 QML Canvas,我们已经学习了如何绘制基本图形、应用样式和颜色、绘制图像等,现在来看一下如何在 Canvas 中绘制文本。 Canvas 提供了两种方式来渲染文本: fillText(text, x, y) 在指定的 (x,y) 位置填充指定的文本 strokeText(text, x, y) 在指定的 (x,y) 位置绘制文本…

基于t分布变异自适应的改进的黏菌算法(TSMA)

目录 一、基于t分布的自适应黏菌优化算法TSAM 1.1 自适应t分布变异 1.2 建立基于t分布的自适应黏菌优化算法TSAM 二、TSMA伪码表示如下&#xff1a; 三、改进对比 黏菌优化算法灵感来自于黏菌的扩张和觅食行为。主要模拟了黏菌在觅食过程中的行为和形态变化&#xff0c;没…

【网络2】MII MDIO

文章目录 1.MII&#xff1a;ISO网络模型中物理层&#xff08;phy&#xff09;和数据链路层&#xff08;mac&#xff09;属于硬件&#xff0c;其余都属于软件kernel2.MDC/MDIO&#xff1a;不仅管phy&#xff0c;只要支持mdio协议都可以管2.1 3.RGMII时序调整&#xff1a;下面波形…

BUUCTF——九连环1

听这个名字就不正常&#xff0c;不会加密九次吧 打开也是一张图片 依旧是存在隐藏文件信息 分离出来后有两个文件&#xff0c;和两个压缩包&#xff0c;但是都需要密码 哦看错了&#xff0c;原来binwalk直接把里面的给分离出来了 所以现在就asd一个文件夹 爆破不出来&#xff0…

为什么调试很重要?gdb调试分析问题

为什么调试很重要&#xff1f; 一、引言二、调试的定义和分类2.1、调试的定义2.2.、调试的分类 三、调试的重要性四、调试的步骤和技巧4.1、定位问题4.2、重现问题4.3、分析问题4.4、解决问题4.5、调试技巧 五、简单的GDB调试示例&#xff1a;六、总结 一、引言 &#x1f4a1; …

【基于Django框架的在线教育平台开发-02】用户注册功能开发

用户注册功能开发 文章目录 用户注册功能开发1 模型层开发2 视图层开发3 配置urls.py4 表单验证5 模板层开发6 效果展示 1 模型层开发 用户数据表如下所示&#xff1a; FieldTypeExtraidintPrime Key & Auto Incrementpasswordvarchar(128)last_logindatetime(6)Allow Nu…

缓存雪崩和缓存穿透的解决方案

缓存雪崩 缓存雪崩是指存储在缓存里面的大量数据&#xff0c;在同一时刻全部过期&#xff0c;大部分流量直接到达了数据库&#xff0c;导致数据库压力增加&#xff0c;造成数据库崩溃的情况。 缓存雪崩的解决方案如下&#xff1a; 每个缓存的key设置不同的过期时间采用多级缓…

STM32单片机(九)USART串口----第四节:USART串口实战练习(串口发送+接收)

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

在 K8S 中部署一个应用 下

接着上一篇继续部署应用到 K8S中 之前简单部署的简单集群&#xff0c;三个工作节点是运行在 docker 和 kubelet 的&#xff0c;还有一个是控制节点 ReplicationController &#xff0c; pod 和 service 本次关系 之前有提到 ReplicationController &#xff0c; pod 和 服务…

设计模式之命令模式笔记

设计模式之命令模式笔记 说明Command(命令)目录命令模式示例类图订单类厨师类抽象命令类订单命令类服务员类测试类 说明 记录下学习设计模式-命令模式的写法。JDK使用版本为1.8版本。 Command(命令) 意图:将一个请求封装为一个对象&#xff0c;从而使得可以用不同的请求对客…

STM32单片机(九)USART串口----第二节:USART串口外设

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

STM32单片机(九)USART串口----第三节:USART串口实战练习(串口发送)

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

常见面试题之MySQL篇

1.MySQL中&#xff0c;如何定位慢查询? 我们当时做压测的时候有的接口非常的慢&#xff0c;接口的响应时间超过了2秒以上&#xff0c;因为我们当时的系统部署了运维的监控系统Skywalking&#xff0c;在展示的报表中可以看到是哪一个接口比较慢&#xff0c;并且可以分析这个接…

【数据库七】MySQL主从复制与读写分离

MySQL主从复制与读写分离 1.案例概述2.什么是读写分离&#xff1f;3.为什么要读写分离呢&#xff1f;4.什么时候要读写分离&#xff1f;5.主从复制与读写分离6.MySQL主从复制原理6.1 mysql的复制类型 7.主从复制的工作过程7.1 MySQL架构图7.2 口语化工作工程 8.MySQL 读写分离原…

VSCode配置C语言编译环境

一、下载C语言编译器&#xff1a; &#xff08;1&#xff09;下载地址&#xff1a;MinGW-w64 - for 32 and 64 bit Windows - Browse /mingw-w64 at SourceForge.net 下载如下的windows版本&#xff1a; &#xff08;2&#xff09;配置环境变量&#xff1a; 二、安装VSCode …