kithara创建并使用内核 DLL
目录
使用内核 DLL与回调函数“command-for-command”重定位函数并生成具有相同语义的副本相比,它更少出错,也有更少的限制。
除了这些优点之外,这也是为所有 64 位 Windows 系统提供的唯一方法。
什么是内核 DLL
一般来说,每个符合 PE(portable executable)标准的 DLL 都可以作为内核 DLL 加载。为什么要将 DLL 加载为内核 DLL 时有一些限制的原因,是它将 DLL 加载到内核地址空间(ring0)。
不适用于内核 DLL 中的函数和命令
功能组/命令 | 适用性 |
所有操作系统函数 | 不可用 |
库函数 | 有限可用(→ 文本) |
用户定义函数 | 有限可用(→ 文本) |
全局数据访问 | 有限可用(→ 文本) |
浮点运算协处理器 | 可用(→ 文本) |
分段数据访问 | 不可用 |
异常处理 | 不可用 |
C++ 类(析构函数) | 可用 |
16 位代码 | 不可用 |
switch 指令 | 可用 |
分支表和间接分支 | 可用 |
函数说明
所有操作系统函数都不能使用(也不能间接使用!)。对于库函数和用户定义函数,同样适用:只有在函数中不被禁止的命令和指令时才能使用。
全局变量
DLL 中的全局变量只能在该 DLL 中使用。无法在应用层(ring3) 上下文中声明这些全局变量为外部变量。
协处理器
使用浮点单元的命令需要特殊处理。因此,所有代码执行都必须使用 【KSF_SAVE_FPU】标志。有三种不同的方式来启动函数的执行,他们都必须使用标志 【KSF_SAVE_FPU】。可以通过 KS_loadKernel函数来设置此标志,在这种情况下,初始化函数可以使用浮点单元。如果从内核 DLL 创建了带有浮点运算的回调,KS_createKernelCallBack函数必须使用 【KSF_SAVE_FPU】标志。如果使用 KS_execKernelFunction 函数调用了具有浮点运算的内核 DLL 时,也必须提供 【KSF_SAVE_FPU】标志。
编码一致
为了能够以较低的成本在操作模式之间切换(应用程序级别/ring3 和内核级别/ring0),请在最初的概念和实现中考虑以下信息:
- 所有数据请求通过引用参数:所有数据应仅通过引用参数 【pArgs】 访问。这个参数(在 C 或 C++ 中)是 void * 类型,但可以立即转换为用户定义数据类型的指针。这样做的原因是所有数据必须位于共享内存中,应用层和内核层中的全局变量并不互通。
- 将所有用户数据汇总到结构中:由于只有一个引用参数可用,因此用户数据方便地组合在一个结构中。
- 始终在共享内存中生成数据:包含用户数据的结构也可以在非内核模式下在共享内存中生成和访问,以避免两种操作模式之间的差异。
- 共享内存中仅使用内核层服务的函数和命令:除了调试模式外,不应使用不是 内核层的函数和命令。对于屏幕打印或其他影响程序顺序的操作,仅能使用 KS_ 函数(如 KS_setEvent、KS_putPipe、KS_logMessage 等)。
内核示例
以下示例代码是 Kithara RealTime Suite 开发/演示版本安装路径中 smp 文件夹中的示例 RealTimeCallBackAsDll 的简化版本。
#include "kernelDLL.h"
// 直接在内核级加载 DLL :全局变量
uint _globalCounter;
// 初始化函数
extern "C" __declspec(dllexport) Error __stdcall _initFunction(void* pArgs) {
_globalCounter = 42;
return KS_OK;
}
// 访问内核 DLL 的全局值
extern "C" __declspec(dllexport) Error __stdcall _getGlobalCounter(void* pArgs) {
*(uint*)pArgs = _globalCounter;
return KS_OK;
}
// 这是回调函数。可以从计时器中调用。
extern "C" __declspec(dllexport) Error
__stdcall _timerCallBack(void* pArgs, void* pContext) {
TimerData* pData = (TimerData*)pArgs; // Casting to TimerData*
if (!pData)
return KSERROR_BAD_PARAM;
++pData->counter_; // Increment our CallBackData counter
++_globalCounter; // Increment our global counter
return KS_OK;
}
// 最后,我们需要实现 DllMain 函数。
// 为了符合 PE(可移植可执行文件)标准,它只是一个占位符。
// 要进行初始化,请定义一个特殊的 init 例程(如上文的 _initFunction
// 并在调用 KS_loadKernel 时指定该例程(稍后将讨论该例程)。
// 或直接通过 KS_execKernelCallBack 调用该例程。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID pReserved) {
return 1;
}
注意:请注意,所有函数应声明为 extern “C”!否则,它们的名称可能无法被加载器找到。为了显示名称,需要通过使用 __declspec(dllexport) 导出它们。
加载 DLL
在弄清楚 DLL 应该是什么样子之后,代码必须编译成 DLL。然后通过Kithara 的机制加载这个 DLL。函数 KS_loadKernel 将 DLL 加载到特定的地址空间。如果成功加载了 DLL,此函数将得到 DLL 句内核柄。
参数 dllName 应该指向要加载的 DLL。可以确定完整路径,包括文件名(例如 C:\DLLs\kernelDLLs\sample.dll),也可以只指定 DLL 文件名。在第二种情况下,DLL 能在调用进程运行的路径以及 Windows PATH 变量的所有路径中搜索到。
如果 DLL 中有初始化函数,则可以在加载 DLL 时运行此函数。将初始化函数的名称提供给 initProcName。可以使用 pArgs 传递参数给初始化函数。
最后一个参数 flags 具有重要意义。在这里确定了 DLL 被加载到哪个地址空间(应用层还是内核层)。此外,还可以激活一些其他选项,这些选项在 API 文档中有描述:KS_loadKernel。下面的标志不能逐位组合:
- KSF_USER_EXEC:将 DLL 加载到应用程序地址空间。所有变量和函数都有 ring3 地址,并将在应用程序级别访问和执行。不加载 DLL 中的任何依赖项。如果需要从此 DLL 生成回调,则只能是 ring3 回调!
- KSF_KERNEL_EXEC:DLL 将加载到内核地址空间。所有变量和函数都有 ring0 地址,并将在内核级别访问和执行。不加载 DLL 中的任何依赖项。如果需要从此 DLL 生成回调,则只能是 ring0 回调!
注意:如果既没有 KSF_USER_EXEC 也没有 KSF_KERNEL_EXEC,则使用标准的 Windows LoadLibrary 函数加载 DLL。Windows 将自行加载所有依赖项。
何时加载
在大多数情况下,DLL 将以 KSF_KERNEL_EXEC 标志加载为内核 DLL。但是,编写一个按预期工作的 DLL 可能是一项漫长的任务。
通过“尽可能相同的编码形式”,应该能够以非常少量的代码更改来更改库加载模式(或者执行级别)。
内核调试
将 DLL 作为普通的 Windows DLL 加载通常很有用。这样,可以使用诸如“MS Visual Studio”之类的调试器来调试程序代码。为此,使用带有调试符号构建 DLL,并在主程序中不使用任何标志调用 KS_loadKernel。将调试器附加到加载 DLL 的主进程并像平常一样进行调试。
Windows LoadLibrary 函数将加载 DLL 中引用的所有依赖项。如果使用调试符号构建了它们,那么所有这些依赖项也可以进行调试。
使用 Kithara 机制加载 DLL
如上所述,使用 KS_loadKernel 的主要原因是希望在 ring0 运行程序代码,甚至可能带有实时上下文。为此,必须设置 KSF_KERNEL_EXEC 标志。如果以这种方式加载了 DLL,则调用的任何函数都将在内核层上执行。如果使用实时上下文启动执行,则代码将在实时级别上处理,例如,存在具有实时访问权限的计时器,该计时器调用内核 DLL 中的函数。
释放DLL
函数 KS_freeKernel 释放了 DLL 使用的所有资源。建议确保来自此 DLL 的所有回调都已移除,并且不再对 DLL 进行任何其他请求。
DLL操作
将 DLL 加载到所需的地址空间后,有几种选项可用于使用 DLL 中的函数。
列举所有可用函数
函数 KS_enumKernelFunctions 可用于检索 DLL 的所有地址。枚举索引从零开始,并将函数的名称写入 pName。请确保 pName 指向至少为 256 字节的字符串缓冲区。函数 KS_enumKernelFunctions 将最多复制 255 个字符 + 零终止符到 pName。因此,DLL 导出函数的名称不应超过 255 个字符。
通过 KS_execKernelFunction 进行立即执行
在 DLL 中执行函数的最直接方式是使用函数 KS_execKernelFunction。该函数将调用名称为 name 的函数。
函数 KS_execKernelFunction 将等待被调用的函数返回,然后才会返回给调用方。KS_execKernelFunction 可以向被调用的函数提供两个参数。第一个参数是由 pArgs 给出的,应该指向被调用函数期望的数据结构。上下文 pContext 是第二个参数。name 中确定的函数可能只使用一个参数 pArgs,但在这种情况下,必须在 KS_execKernelFunction 的 flags 参数中提供标志 KSF_NO_CONTEXT。
KS_execKernelFunction 可以从 ring3 或 ring0 上下文调用。但是,在 ring0 上下文中,只能访问加载到 ring0 的 DLL(KS_KERNEL_EXEC)。
如果调用的是内核 DLL(加载到 ring0/kernel 地址空间的 DLL),则可以使用 KS_execKernelFunction 的 flags 参数来提供其他标志:
- KSF_REALTIME_EXEC:调用的函数在实时任务环境中执行。
- KSF_SAVE_FPU:如果调用的函数直接或间接使用协处理器(例如用于浮点运算),则必须提供此标志。
查询函数地址
函数 KS_getKernelFunction 用于确定加载的 DLL 的函数地址。根据加载 DLL 的级别,将返回 ring0 或 ring3 函数地址。要查询的函数由 name 中确定,地址将返回给 pRelocated。
注意:必须在正确的执行上下文中调用检索到的函数地址。这意味着 ring3 函数地址只能从 ring3 调用,而 ring0 函数地址只能从 ring0 调用。
同步内核执行
程序的各个部分经常需要访问硬件,例如在内核级别执行的 ISR 和应用程序中的函数。如果访问不能被中断,那么它必须是一个“原子”操作。您可以使用 KS_execSyncFunction 函数来实现这一点。
KS_execSyncFunction 将通过 proc(使用 KS_getKernelFunction 检索的函数地址)将给定函数与由 hHandler 给出的 ISR 同步。同步函数在内核级别执行。由于这个原因,包含函数的 DLL 必须加载到内核地址空间。
所有交换的数据应该存储在共享内存空间中。使用 pArgs 提供共享内存的地址。
生成回调对象
在将 DLL 加载到所需地址空间后,可以为相同地址空间创建回调对象。这些回调可以用于多个任务,如 ISR 或事件处理等。
向 name 提供回调的函数名称,并向 pArgs 提供参数指针,KS_createKernelCallBack 将返回一个回调句柄给 phCallBack。如果提供了标志 KSF_DIRECT_EXEC,则回调的执行在实时上下文中完成。这仅在将 DLL 加载到 ring0 时才可能。对于所有使用协处理器直接或间接进行浮点运算的 ring0 回调,必须设置标志 KSF_SAVE_FPU。
这些回调对象在不再使用时需要使用 KS_removeCallBack进行移除。
项目实例
- 项目结构:
KitharaDemo是一个可执行文件,KitharaDll是一个动态库,他们需要Kithara相关环境才能执行。
完整代码如下:
KitharaDemo.cpp
#include <iostream>
#include <KrtsDemo.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
const char customerNumber[256] = "DEMO";
void OutputErr(KSError error, const char* pFuncName, const char* pComment)
{
if (error == KS_OK)
return;
const char* pError;
KS_getErrorString(error, &pError, KSLNG_DEFAULT);
printf("ERROR (%08X = \'%s\') - %s: %s\n", error, pError, pFuncName, pComment);
}
int main()
{
KSError error;
error = KS_openDriver(customerNumber); // 所有Kithar项目的第一步
if (error != KS_OK)
{
error = 1;
OutputErr(error, "KS_openDriver", "Unable to open the driver!");
}
else
{
printf("Hello Kithara! \n");
}
// 加载内核DLL
KSHandle kermel_handle;
error = KS_loadKernel(&kermel_handle, "KitharaDll.dll", "_initFunction", NULL, KSF_KERNEL_EXEC | KSF_SAVE_FPU);
if (error != KS_OK)
{
OutputErr(error, "KS_loadKernel", "load dll failed!");
KS_closeDriver();
return 0;
}
int args;
error = KS_execKernelFunction(kermel_handle, "_getGlobalCounter", &args, NULL, KSF_NO_CONTEXT);
if (error != KS_OK)
{
OutputErr(error, "KS_execKernelFunction", "get args failed");
KS_closeDriver();
return 0;
}
printf("this is kernel args: %d \n", args);
// 资源释放
error = KS_freeKernel(kermel_handle);
if (error != KS_OK)
{
OutputErr(error, "KS_freeKernel", "");
KS_closeDriver();
return error;
}
error = KS_closeDriver();
if (error != KS_OK)
{
OutputErr(error, "KS_closeDriver", "");
KS_closeDriver();
return error;
}
Sleep(1000);
return 0;
}
KitharaDll.cpp
注意:内核层Dll生成库为MT
#include <cstdint>
#include <KrtsDemo.h>
int _globalCounter;
// 初始化函数
extern "C" __declspec(dllexport) KSError __stdcall _initFunction(void* pArgs) {
_globalCounter = 42;
return KS_OK;
}
// 访问内核 DLL 的全局值
extern "C" __declspec(dllexport) KSError __stdcall _getGlobalCounter(void* pArgs) {
*(uint*)pArgs = _globalCounter;
return KS_OK;
}
#define WIN32_LEAN_AND_MEAN
// Windows 头文件
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID pReserved) {
return TRUE;
}
更多实例:
smp\EtherCATBasics