9.2 Windows驱动开发:内核解析PE结构导出表

news2024/9/24 1:16:34

在笔者的上一篇文章《内核特征码扫描PE代码段》LyShark带大家通过封装好的LySharkToolsUtilKernelBase函数实现了动态获取内核模块基址,并通过ntimage.h头文件中提供的系列函数解析了指定内核模块的PE节表参数,本章将继续延申这个话题,实现对PE文件导出表的解析任务,导出表无法动态获取,解析导出表则必须读入内核模块到内存才可继续解析,所以我们需要分两步走,首先读入内核磁盘文件到内存,然后再通过ntimage.h中的系列函数解析即可。

PE结构(Portable Executable Structure)是Windows操作系统用于执行可执行文件和动态链接库(DLL)的标准格式。导出表(Export Table)是PE结构中的一个部分,它记录了一个DLL中所有可供外部调用的函数和变量。

导出表通常位于PE结构的数据目录中。它包含两个重要的表格:导出名称表格和导出地址表格。导出名称表格列出了DLL中所有导出函数和变量的名称,而导出地址表格列出了这些函数和变量的内存地址。

当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。导出函数在DLL文件中,导出信息被保存在导出表,导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器能够通过这些信息来完成动态链接的整个过程。

导出函数存储在PE文件的导出表里,导出表的位置存放在PE文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是导出表的RVA值,导出表同样可以使用函数名或序号这两种方法导出函数。

导出表的起始位置有一个IMAGE_EXPORT_DIRECTORY结构,与导入表中有多个IMAGE_IMPORT_DESCRIPTOR结构不同,导出表只有一个IMAGE_EXPORT_DIRECTORY结构,该结构定义如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;        // 文件的产生时刻
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;                  // 指向文件名的RVA
    DWORD   Base;                  // 导出函数的起始序号
    DWORD   NumberOfFunctions;     // 导出函数总数
    DWORD   NumberOfNames;         // 以名称导出函数的总数
    DWORD   AddressOfFunctions;    // 导出函数地址表的RVA
    DWORD   AddressOfNames;        // 函数名称地址表的RVA
    DWORD   AddressOfNameOrdinals; // 函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

其中,Name字段指向了该DLL的名称字符串,Base字段为该DLL的加载基地址,NumberOfFunctionsNumberOfNames分别表示导出函数和变量的数量,AddressOfFunctions、AddressOfNamesAddressOfNameOrdinals则是三个表格的地址。

总的来说,导出表是DLL中非常重要的一个部分,它提供了一种方便的方法,使其他程序可以调用DLL中的函数和变量。

上面的_IMAGE_EXPORT_DIRECTORY 结构如果总结成一张图,如下所示:

在上图中最左侧AddressOfNames结构成员指向了一个数组,数组里保存着一组RVA,每个RVA指向一个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的结构成员,该对应项存储的正是函数的唯一编号并与AddressOfFunctions结构成员相关联,形成了一个导出链式结构体。

获取导出函数地址时,先在AddressOfNames中找到对应的名字MyFunc1,该函数在AddressOfNames中是第1项,然后从AddressOfNameOrdinals中取出第1项的值这里是1,然后就可以通过导出函数的序号AddressOfFunctions[1]取出函数的入口RVA,然后通过RVA加上模块基址便是第一个导出函数的地址,向后每次相加导出函数偏移即可依次遍历出所有的导出函数地址。

其解析过程与应用层基本保持一致,如果不懂应用层如何解析也可以去看我以前写过的《PE格式:手写PE结构解析工具》里面具体详细的分析了解析流程。

首先使用InitializeObjectAttributes()打开文件,打开后可获取到该文件的句柄,InitializeObjectAttributes宏初始化一个OBJECT_ATTRIBUTES结构体, 当一个例程打开对象时由此结构体指定目标对象的属性,此函数的微软定义如下;

VOID InitializeObjectAttributes(
  [out]          POBJECT_ATTRIBUTES   p,      // 权限
  [in]           PUNICODE_STRING      n,      // 文件名
  [in]           ULONG                a,      // 输出文件
  [in]           HANDLE               r,      // 权限
  [in, optional] PSECURITY_DESCRIPTOR s       // 0
);

当权限句柄被初始化后则即调用ZwOpenFile()打开一个文件使用权限FILE_SHARE_READ打开,打开文件函数微软定义如下;

NTSYSAPI NTSTATUS ZwOpenFile(
  [out] PHANDLE            FileHandle,         // 返回打开文件的句柄
  [in]  ACCESS_MASK        DesiredAccess,      // 打开的权限,一般设为GENERIC_ALL。
  [in]  POBJECT_ATTRIBUTES ObjectAttributes,   // OBJECT_ATTRIBUTES结构
  [out] PIO_STATUS_BLOCK   IoStatusBlock,      // 指向一个结构体的指针。该结构体指明打开文件的状态。
  [in]  ULONG              ShareAccess,        // 共享的权限。可以是FILE_SHARE_READ 或者 FILE_SHARE_WRITE。
  [in]  ULONG              OpenOptions         // 打开选项,一般设为 FILE_SYNCHRONOUS_IO_NONALERT。
);

接着文件被打开后,我们还需要调用ZwCreateSection()该函数的作用是创建一个Section节对象,并以PE结构中的SectionALignment大小对齐映射文件,其微软定义如下;

NTSYSAPI NTSTATUS ZwCreateSection(
  [out]          PHANDLE            SectionHandle,            // 指向 HANDLE 变量的指针,该变量接收 section 对象的句柄。
  [in]           ACCESS_MASK        DesiredAccess,            // 指定一个 ACCESS_MASK 值,该值确定对 对象的请求访问权限。
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,         // 指向 OBJECT_ATTRIBUTES 结构的指针,该结构指定对象名称和其他属性。
  [in, optional] PLARGE_INTEGER     MaximumSize,              // 指定节的最大大小(以字节为单位)。
  [in]           ULONG              SectionPageProtection,    // 指定要在 节中的每个页面上放置的保护。 
  [in]           ULONG              AllocationAttributes,     // 指定确定节的分配属性的SEC_XXX 标志的位掩码。 
  [in, optional] HANDLE             FileHandle                // (可选)指定打开的文件对象的句柄。
);

最后读取导出表就要将一个磁盘中的文件映射到内存中,内存映射核心文件时ZwMapViewOfSection()该系列函数在应用层名叫MapViewOfSection()只是一个是内核层一个应用层,这两个函数参数传递基本一致,以ZwMapViewOfSection为例,其微软定义如下;

NTSYSAPI NTSTATUS ZwMapViewOfSection(
  [in]                HANDLE          SectionHandle,          // 接收一个节对象
  [in]                HANDLE          ProcessHandle,          // 进程句柄,此处使用NtCurrentProcess()获取自身句柄
  [in, out]           PVOID           *BaseAddress,           // 指定填充地址
  [in]                ULONG_PTR       ZeroBits,               // 0
  [in]                SIZE_T          CommitSize,             // 每次提交大小 1024
  [in, out, optional] PLARGE_INTEGER  SectionOffset,          // 0
  [in, out]           PSIZE_T         ViewSize,               // 浏览大小
  [in]                SECTION_INHERIT InheritDisposition,     // ViewShare
  [in]                ULONG           AllocationType,         // 分配类型 MEM_TOP_DOWN
  [in]                ULONG           Win32Protect            // 权限 PAGE_READWRITE(读写)
);

将如上函数研究明白那么代码就变得很容易了,首先InitializeObjectAttributes设置文件权限与属性,然后调用ZwOpenFile打开文件,接着调用ZwCreateSection创建节对象,最后调用ZwMapViewOfSection将磁盘文件映射到内存,这段代码实现起来很简单,完整案例如下所示;

#include <ntifs.h>
#include <ntimage.h>
#include <ntstrsafe.h>

// 内存映射文件
NTSTATUS KernelMapFile(UNICODE_STRING FileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress)
{
    NTSTATUS status = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    OBJECT_ATTRIBUTES objectAttr = { 0 };
    IO_STATUS_BLOCK iosb = { 0 };
    PVOID pBaseAddress = NULL;
    SIZE_T viewSize = 0;

    // 设置文件权限
    InitializeObjectAttributes(&objectAttr, &FileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    // 打开文件
    status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttr, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
    if (!NT_SUCCESS(status))
    {
        return status;
    }

    // 创建节对象
    status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile);
    if (!NT_SUCCESS(status))
    {
        ZwClose(hFile);
        return status;
    }
    // 映射到内存
    status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);
    if (!NT_SUCCESS(status))
    {
        ZwClose(hSection);
        ZwClose(hFile);
        return status;
    }

    // 返回数据
    *phFile = hFile;
    *phSection = hSection;
    *ppBaseAddress = pBaseAddress;

    return status;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    NTSTATUS status = STATUS_SUCCESS;

    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    PVOID pBaseAddress = NULL;
    UNICODE_STRING FileName = {0};

    // 初始化字符串
    RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");

    // 内存映射文件
    status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
    if (NT_SUCCESS(status))
    {
        DbgPrint("读取内存地址 = %p \n", pBaseAddress);
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

运行这段程序,即可读取到ntoskrnl.exe磁盘所在文件的内存映像基地址,效果如下所示;

如上代码读入了ntoskrnl.exe文件,接下来就是解析导出表,首先将pBaseAddress解析为PIMAGE_DOS_HEADER获取DOS头,并在DOS头中寻找PIMAGE_NT_HEADERS头,接着在NTHeader头中得到数据目录表,此处指向的就是导出表PIMAGE_EXPORT_DIRECTORY通过pExportTable->NumberOfNames可得到导出表的数量,通过(PUCHAR)pDosHeader + pExportTable->AddressOfNames得到导出表的地址,依次循环读取即可得到完整的导出表。

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    NTSTATUS status = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    PVOID pBaseAddress = NULL;
    UNICODE_STRING FileName = { 0 };
    LONG FunctionIndex = 0;

    // 初始化字符串
    RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");

    // 内存映射文件
    status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
    if (NT_SUCCESS(status))
    {
        DbgPrint("[LyShark] 读取内存地址 = %p \n", pBaseAddress);
    }

    // Dos 头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;

    // NT 头
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);

    // 导出表
    PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);

    // 有名称的导出函数个数
    ULONG ulNumberOfNames = pExportTable->NumberOfNames;
    DbgPrint("[lyshark] 导出函数个数: %d \n\n", ulNumberOfNames);

    // 导出函数名称地址表
    PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
    PCHAR lpName = NULL;

    // 开始遍历导出表(输出ulNumberOfNames导出函数)
    for (ULONG i = 0; i < ulNumberOfNames; i++)
    {
        lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);

        // 获取导出函数地址
        USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
        ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
        PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);

        // 获取SSDT函数Index
        FunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);

        DbgPrint("序号: [ %d ] | Hint: %d | 地址: %p | 函数名: %s \n", i, uHint, lpFuncAddr, lpName);
    }

    // 释放指针
    ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
    ZwClose(hSection);
    ZwClose(hFile);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码运行后即可获取到当前ntoskrnl.exe程序中的所有导出函数,输出效果如下所示;

  • SSDT表通常会解析\\??\\C:\\Windows\\System32\\ntoskrnl.exe
  • SSSDT表通常会解析\\??\\C:\\Windows\\System32\\win32k.sys

根据上方的函数流程将其封装为GetAddressFromFunction()用户传入DllFileName指定的PE文件,以及需要读取的pszFunctionName函数名,即可输出该函数的导出地址。

// 寻找指定函数得到内存地址
ULONG64 GetAddressFromFunction(UNICODE_STRING DllFileName, PCHAR pszFunctionName)
{
    NTSTATUS status = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    PVOID pBaseAddress = NULL;

    // 内存映射文件
    status = KernelMapFile(DllFileName, &hFile, &hSection, &pBaseAddress);
    if (!NT_SUCCESS(status))
    {
        return 0;
    }
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
    ULONG ulNumberOfNames = pExportTable->NumberOfNames;
    PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
    PCHAR lpName = NULL;

    for (ULONG i = 0; i < ulNumberOfNames; i++)
    {
        lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
        USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
        ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
        PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);

        if (_strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)) == 0)
        {
            ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
            ZwClose(hSection);
            ZwClose(hFile);

            return (ULONG64)lpFuncAddr;
        }
    }
    ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
    ZwClose(hSection);
    ZwClose(hFile);
    return 0;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    UNICODE_STRING FileName = { 0 };
    ULONG64 FunctionAddress = 0;

    // 初始化字符串
    RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");

    // 取函数内存地址
    FunctionAddress = GetAddressFromFunction(FileName, "ZwQueryVirtualMemory");
    DbgPrint("ZwQueryVirtualMemory内存地址 = %p \n", FunctionAddress);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

如上程序所示,当运行后即可获取到ntdll.dll模块内ZwQueryVirtualMemory的导出地址,输出效果如下所示;

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

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

相关文章

vue实现动态路由菜单!!!

目录 总结一、步骤1.编写静态路由编写router.jsmain.js注册 2.编写permisstions.js权限文件编写permisstions.jsaxios封装的APIstore.js状态库system.js Axios-APIrequest.js axios请求实例封装 3.编写菜单树组件MenuTree.vue 4.主页中使用菜单树组件 总结 递归处理后端响应的…

java基础-IO

1、基础概念 1.1、文件(File) 文件的读写可以说是开发中必不可少的部分&#xff0c;因为系统会存在大量处理设备上的数据&#xff0c;这里的设备指硬盘&#xff0c;内存&#xff0c;键盘录入&#xff0c;网络传输等。当然这里需要考虑的问题不仅仅是实现&#xff0c;还包括同步…

人工智能|机器学习——机器学习如何判断模型训练是否充分

一、查看训练日志 训练日志是机器学习中广泛使用的训练诊断工具&#xff0c;每个 epoch 或 iterator 结束后&#xff0c;在训练集和验证集上评估模型&#xff0c;并以折线图的形式显示模型性能和收敛状况。训练期间查看模型的训练日志可用于判断模型训练时的问题&#xff0c;例…

IOC DI入门

1.加上Component&#xff0c;控制翻转&#xff0c;将service和dao都交给IOC容器管理&#xff0c;成为IOC容器中的bean。用哪个类就在哪个类上面加component。 2.加上autowired。依赖注入。controller依赖于service&#xff0c;service依赖于dao。加上时&#xff0c;IOC容器会提…

Taro3+Vue3重构Mpvue小程序项目踩坑记

1、Taro小程序编译时报错&#xff1b; 原因:页面中存在小程序识别不了的标签&#xff1b;如div解决方法&#xff1a; 将div标签替换成小程序可识别的标签&#xff1b; 安装Taro中提供的插件:tarojs/plugin-html, 使其可被识别&#xff1b; 插件安装教程参考Taro官网&#xff1…

Matlab 点云曲率计算(之二)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 之前已经讨论过许多关于计算曲率的问题,这里使用一个通过拟合三次曲面方程的方式来计算曲率,计算过程如下图所示: 二、实现代码 %********

springboot+bootstarp+jsp房屋租赁系统ssm_t65a9

小型房屋租赁系统主要有管理员、房东和租户三个功能模块。以下将对这三个功能的作用进行详情的剖析。 管理员模块&#xff1a;管理员是系统中的核心用户&#xff0c;管理员登录后&#xff0c;可以对后台系统进行管理。主要功能有个人中心、房东管理、租户管理、房源城市管理、房…

数据库应用:MongoDB 库与集合管理

目录 一、理论 1.MongoDB用户管理 2.MogoDB库管理 3.MogoDB集合管理 二、实验 1.MongoDB用户管理 2.MogoDB库管理 3.MogoDB集合管理 三、问题 1.不显示新创建的数据库 2.插入数据报错 3.删除指定数据库报错 一、理论 1.MongoDB用户管理 (1) 内置角色 数据库用户…

什么是高级语言、机器语言、汇编语言?什么是编译和解释?

1、高级语言 计算机程序是一种让计算机执行特定任务的方法。程序是由程序员用一种称为编程语言的特殊语言编写的。编程语言有很多种&#xff0c;例如 C、C、Java、Python 等。这些语言被称为高级语言&#xff0c;因为它们更接近人类的自然语言&#xff0c;而不是计算机能够直接…

【LeetCode刷题】--38.外观数列

38.外观数列 方法&#xff1a;遍历生成 该题本质上是依次统计字符串中连续相同字符的个数 例如字符串 1112234445666我们依次统计连续相同字符的个数为: 3 个连续的字符 1, 222 个连续的 2&#xff0c;1 个连续的字符 3&#xff0c;3个连续的字符 4&#xff0c;1个连续的字符…

创建一个带有背景图层和前景图层的渲染窗口

开发环境&#xff1a; Windows 11 家庭中文版Microsoft Visual Studio Community 2019VTK-9.3.0.rc0vtk-example demo解决问题&#xff1a; 创建一个带有背景图层和前景图层的渲染窗口&#xff0c;知识点&#xff1a;1. 画布转image&#xff1b;2. 渲染图层设置&#xff1b;3.…

如何高效批量生成条形码?

条形码作为商品、库存和信息管理的基础工具&#xff0c;扮演着至关重要的角色。为了满足用户对于高效、专业、多样化的条形码生成需求&#xff0c;我们推出了一款专业高效的在线条形码生成工具。 网址&#xff1a;https://www.1txm.com/ 多样化条形码支持 易条形支持多种常见…

Django请求生命周期流程

浏览器发起请求。 先经过网关接口&#xff0c;Django自带的是wsgiref&#xff0c;请求来的时候解析封装&#xff0c;响应走的时候打包处理&#xff0c;这个wsgiref模块本身能够支持的并发量很少&#xff0c;最多1000左右&#xff0c;上线之后会换成uwsgi&#xff0c;并且还会加…

Redis 主库挂了,如何不间断服务?

目录 1、哨兵机制的基本流程 2、主观下线和客观下线 3、如何选定新的主库&#xff1f; 总结 // 你只管前行&#xff0c;剩下的交给时间 在 reids 主从库集群模式下&#xff0c;如果从库发生故障了&#xff0c;客户端可以继续向主库或其他从库发送请求&#xff0c;进行相关的…

宠物网站的技术 SEO:完整指南

您是宠物行业网站的从业者吗&#xff1f;那么您一定知道&#xff0c;当人们寻找与宠物相关的资源时&#xff0c;在搜索引擎结果中排名靠前有多么重要。 这就是技术SEO的用武之地&#xff01;它正在调整您网站的后端代码和服务器配置&#xff0c;以在 SERP 中排名更高。 在此&…

PCF8591多通道数据读取异常问题

问题描述 PCF8591在循环读取两个通道时&#xff0c;两个通道数据出现交错问题。 例如我们想实现&#xff1a;第一次读取通道一、第二次读取通道二、第三次读取通道一、第四次读取通道二……依次循环 但实际数据&#xff1a;第一次读取的值为0x80、第二次读取的值为通道一的值、…

西南科技大学C++程序设计实验二(类与对象一)

C++最大的特点就是面向对象,掌握它的几种基本性质还是好理解的,可以看我C++专栏的期末速成,希望对你们学习C++有帮助。 一、实验目的 1.理解简单类的定义、说明与使用 2.理解类中不同属性数据成员的访问特点 3.理解构造函数、析构函数的作用 重点:掌握类的定义与实现,…

java多线程-扩展知识一:进程线程、并发并行、同步异步

1、进程 进程&#xff08;Process&#xff09;是计算机中的程序关于某数据集合上的一次运行活动&#xff0c;是系统进行资源分配的基本单位&#xff0c;是操作系统结构的基础。在早期面向进程设计的计算机结构中&#xff0c;进程是程序的基本执行实体&#xff1b;在当代面向线程…

前端入门(三)Vue组件化编程、脚手架、插槽插件、存储、vuex、组件事件、动画、代理

文章目录 Vue 组件化编程 - .vue文件非单文件组件组件的注意点组件嵌套Vue实例对象和VueComponent实例对象Js对象原型与原型链Vue与VueComponent的重要内置关系 应用单文件组件构建 Vue脚手架 - vue.cli项目文件结构组件相关高级属性引用名 - ref数据接入 - props混入 - mixin …

轻巧高效的剃须好工具,DOCO黑刃电动剃须刀上手

剃须刀大家都用过&#xff0c;我比较喜欢电动剃须刀&#xff0c;尤其是多刀头的悬浮剃须刀&#xff0c;感觉用起来很方便&#xff0c;剃须效率也很高。最近我在用一款DOCO小蔻的黑刃电动剃须刀&#xff0c;这款剃须刀轻巧易用&#xff0c;而且性价比超高。 相比于同类产品&…