【干货】如何开发一款游戏修改器?17:win32 DLL动态链接库
文摘
游戏
2025-01-10 00:00
广西
本期我们来讨论一下动态链接库(DLL)的概念。动态链接库用来注入我们的代码是非常方便的,特别是相比使用EXE跨进程注入代码来说,DLL的控制更加方便。DLL的优势在于它可以被动态地加载到其他可执行程序中,而且特别适合注入带窗口的代码。下面我们先了解一下DLL的概念。动态链接库(DLL)在Windows系统及应用程序中被广泛使用。但是它有一个特点,就是动态链接库不能单独执行,它是依赖于其他可执行程序的。一般来说,它指的是EXE程序的依赖,比如我们系统的动态链接库。当我们附加一个游戏进程时,例如使用xd bug,可以在符号选项卡中看到很多动态链接库。这些动态链接库的存在形式多种多样,一般我们能看到的是系统目录下的动态链接库,也就是系统模块;而用户模块指的是不在系统目录下的动态链接库,也可以理解为自定义的动态链接库。我们自己也可以编写类似这样的动态链接库。动态链接库有一套固定的函数,包括一些初始化和清理的函数。接下来,我们会稍微了解一下动态链接库的几个重要事件。首先,当我们将动态链接库加载到内存时,它会执行一个特定的入口点函数。这个入口点函数的编号一般是21234或其他整数值,它标识着动态链接库的加载。对于更多详细的说明,我们可以在网络上搜索动态链接库的相关信息,有时会找到比较标准的解释。接下来,我们打开Visual Studio 2017,并在文件新建项目中选择“Win32动态链接库”。这个项目会自动生成一个Win32动态链接库的入口点。这个入口点与我们之前看到的EXE程序的入口点类似,但是在动态链接库加载时会调用它。动态链接库的入口点包含四个不同的事件,我们稍后会对它们进行简单说明。其中,第一个事件会在动态链接库加载时执行,我们会在控制台中配合说明一下这个动作。在这里,我们可以给入口点写一个简单的函数,并加上标准输入输出库(STDLC)的头文件。我们可以在代码中加上一些注释,以便理解。总的来说,动态链接库的加载事件会在动态链接库被加载到内存时执行一次,而动态链接库的加载是通过load library函数实现的。那么我们来讨论一下第二个事件。当我们创建一个新的线程时,好像它会进入到这个case里面。让我们看一下网上的解释,找一下关于“third touch”的信息。根据网上的解释,当当前线程正在创建新线程时,系统会调用当前附加到进程的所有DLL的入口点函数。所以当一个新线程被创建时,它会执行到这个编号为K12的地方。这意味着有新的线程被创建时会执行这个部分。同样地,当一个新的线程结束时,也应该会执行到这个地方。这描述了线程的创建和退出。接下来,让我们来适当地解释一下编号为零的事件。当我们的动态链接库正在被卸载时,会执行到这个部分。这意味着系统正在释放这个动态链接库的内存,可能是因为加载失败或者进程终止。虽然描述不是很容易理解,但大致意思就是当我们调用“free library”来释放动态链接库时,会执行到这个部分。由于我们对这四个事件的理解并不完全,我们可以尝试添加一个新的控制台程序来加深对这些事件的理解。让我们创建一个名为“测试DL”的控制台程序,并将这四个事件的入口点添加到其中。接下来,我们需要包含Windows的头文件,以便使用其中的两个API函数:LoadLibrary和FreeLibrary。LoadLibrary用于加载动态链接库到进程空间,而FreeLibrary用于释放动态链接库。这两个函数是相反的,一个是加载,一个是释放。我们先加载我们的动态链接库,然后它会返回一个指向动态链接库的句柄。接着我们可以随意给它取一个名字。当然,我们也可以使用完整的路径名。在调用这些API函数时,需要注意它们有两个版本,一个是ANSI版本(A版本),一个是宽字符版本(W版本)。我们根据字符集的设置来选择使用哪个版本。在属性设置中,我们可以修改字符集来选择使用ANSI版本还是宽字符版本。现在,我们尝试编译这个控制台程序,看看能否成功。在使用字符集时,如果我们正在使用ANSI版本(A版本),那么我们的字符串格式就会像这样。但如果我们使用宽字符版本(W版本),那么字符串的后面我们需要怎么写呢?一般来说,我们需要在字符串前面加一个L
,然后动态链接库的调用一般会是这样写的。对于A版本,它的常量字符串是没有L
的,比如像这样"Hello, world!"
。现在我们的写法是针对A版本的字符串格式,所以我们需要改变字符集设置。除了改变字符集之外,我们还可以显式地调用多字节字符集的版本。另一种方法是,如果我们还坚持使用多字节字符集的字符串格式,那么应该怎么做呢?这时候,我们可以显式地调用LoadLibraryA
。这样做是可以的。但是要注意,由于这两个变量同名,我们需要在代码中添加作用域限定符,比如在代码块中。如果我们不去掉这个括号,它们就会指向同一个变量。总之,字符集的选择是根据Windows中的API函数的参数来确定的。如果参数带有字符串,它就会分为A版本和W版本。例如,MessageBox
函数的标题和内容参数都是字符串。如果当前的字符集设置为UI库,那么实际上就是调用W版本的函数。所以,我们可以在字符串前面加上L
来表示这是一个宽字符字符串,这样编译就能通过。另一种方法是显式地调用A版本的函数。但是这些只是对字符集的一些讨论,我们的重点是研究动态链接库的入口点,即这四个事件。我们主要使用的是第一个事件,即当动态链接库加载时触发的事件。后面三个事件几乎用不到。因此,我们应该重点理解第一个事件。至于后面的事件,你可以通过搜索来查找更多信息。现在,让我们写一些代码来测试。我们注释掉LoadLibraryW
这行代码,然后只使用LoadLibrary
来加载DIO
。首先,我们打印出返回值,即动态链接库的地址。同时,这个句柄也是这个动态链接库在内存中加载的地址。让我们先将其设置为启动项。可能会遇到一些问题,但我们先检查一下,如果有问题,再来解决。如果返回的值是零,就证明加载失败了。为什么会失败呢?可能是因为没有找到这个动态链接库。尽管我们看到这个EXE应该在同一个目录中,但我们需要确认一下。现在,让我们重新生成这个DLL并查看一下。现在有了吧。运行一下这个动态链接库。现在你看到有返回值了。HD2找到了这个动态链接库。同时,你可以看到打印出的信息,说明Processor Touch在加载动态链接库时执行了一次。其他的就不用管了。另外,在程序结束时,这个动态链接库也会被释放掉。因为整个控制台程序在执行到第22行时已经结束了。所以,最后在程序退出时,这个动态链接库也会被释放掉。总之,我们只需要了解这一个重点就好了。就是使用LoadLibrary或者LoadLibraryW来加载我们的DLL1时,它会触发一次事件。如果我们调用两次会怎样呢?我们给它加一个行号,然后再次加载。这次我们取消掉MessageBox,这样可能就不会产生新的线程了。我们再试一下。这次我们在后面加上行号。重新编译整个解决方案,然后运行。从测试信息中可以看出,第13行和第16行都执行了,LoadLibrary也调用了两次,两次都返回的是同一个地址,61b7,这就是我们动态链接库加载到内存中后的地址。同时,这个句柄有什么作用呢?我们后面会有相应的说明,暂时不讲这个。那么我们可以看到,Dio Touch实际上只被调用了一次。这说明什么呢?说明即使你多次调用LoadLibrary,它的入口点也只会执行一次。那么什么情况下会多次执行呢?就是当你调用了FreeLibrary释放之后,再次用LoadLibrary加载时,它会再次执行一次。如果已经加载了,那么再次调用LoadLibrary时,就不会再次触发Dio Processor Touch事件来执行它。让我们再来试一下。我们稍微修改一下代码。在这里,我们加一个getchar,即等待用户按下回车键。然后我们用FreeLibrary释放之前的动态链接库,需要传入LoadLibrary的返回值,也就是这个模块的句柄,这样才能释放。如果释放了,然后再次加载,这个时候,Dio Processor Touch就会再次被触发,而且第二次加载之后,地址可能会不一样。好,现在我们应该停在getchar这里。按下一个回车。按下回车后,我们可以看到,好像还是加载到了同一个地址,可能是时间不够还是怎么回事。在后面这里,我们换一下位置,再次重新编译,看一下。正常情况下,如果他释放完了,重新加载之后,地址有可能会变动,但也可能不变动。但是第二次加载的时候,理论上他应该会再次触发Dio Processor Touch,重新进入它的入口点才对。我们再次编译生成,看一下。在16行和18行释放之后,应该会进入另外的事件里面,但是他并没有进去,这个也比较奇怪。没有释放掉。同时,这个句柄也是这个动态链接库在内存中加载的地址。我们先来看一下,把它设置为启动项。这个时候可能不会有我们想的那么顺利,我先看一下,有问题的话我们再解决。那么你看这个时候如果这个值返回是零的话,就证明了加载失败了。失败了为什么会失败呢,因为他没有找到,没有找到这个动态链接库。虽然说我们去看一下,你看虽然说我们去看的时候,这个德尔火这个EXE应该是在同一个目录里面,它才能够加载。我们再重新把这个DL生成一下,但是这个呢是在DLL1debug里面,我们再来看一下,现在有了是吧,有了之后我们再来运行一下这个动态链接库,有了,那么有了之后呢,现在你看是不是有返回值了,HD2找到存在了动态链接库,而且同时呢你看这里打印出来的啊,这个是跌落了processor塌起来,DL被加载时执行了一次,其他的没有住房的。当然后面我们继续执行的话,他就会有新的线程创建啊,具体这个新的线程呢我们但是嗯需要x t bug去详细的分析,才知道有哪些新的线程创建,但是那些呢我们说的嗯我们不用去关心。另外呢这里最后你看这个程序结束的时候,这个动态链接库呢它被释放了啊,因为实际上呢是我们整个控制台的程序执行到这个22行的时候,它已经结束了结束了,所以说最后在程程序退出的时候,这个动态链接库呢也会从内存里面给释放掉,释放掉,好那么我们说的重点的话,我们只需要去了解这一个就行了,这个也就是我们的load library或者是load libraryIn lolibrary w用它来加在我们这个DL1的时候,它会触发一次,那么如果我们有两次die了是吧,有两次会怎么样,好我们给他加一个行号进去,好这一次我们在这里再给它加一次,那么这个时候load library这个函数的一共就调用了两次,同样的这个mage box我们把它给取消掉,取消掉的话可能就没有新的线程会产生的啊,刚才产生的新线程的话可能是由于这个message box来产生的。我们试一下,那么我们两次加载之后呢,这个行号呢他打印的不对啊,我们是因为少写了一个行号在后面,下划线这也是两条,这样我们才把行号给夹在后面,我们重新来执行一次,但是呢我们可以看到啊,重新编译整个解决方案然后再来运行,从这里的测试信息来看的话13行16行都执行了,Load library也执行了两次,两次都返回的是同一个地址,61b7,这个清明明就是我们动态链接库在路程里面被加载之后的路程地址。最后才释放这个FreeLibrary好像没有成功的啊。在这个地方,我们直接把这个getchar项给去掉,直接看一下。理论上我们FreeLibrary的话,他会触发这个事件,但是也没有触发到。他是最后程序退出的时候才触发的。这个事件应该是,我还是把它加上再试一遍。我们在单独的环境下试一下,就直接到这里来执行测试。18行应该执行到18行这里,但是这个FreeLibrary应该也执行了,但FreeLibrary它有一个返回值。我们来看一下这个FreeLibrary它是否是否正常的现象,还有一个返回值。如果他释放成功的话,那么我们在这里给他打印一段信息,看这个释放的结果是是否是成功的释放结果。重新生成一下,然后在当前的环境我们试一下释放。结果他都是451的,是成功了。第19行这里,但是释放的时候他好像是没有尽到我们的那个。我们再来看一下他的这个相关的一个解释。刚才我们看到Unload detach mole,现有现成不会调用新的DNA了。这是DLC,还有个一个这个,每次调用LoadLibrary都会终止来个参数。按照这里的说法的话,我们在的时候他应该会进入到这里面,但是这个时候他没有进去。我们来看一下动态链接库这边写的有没有问题。从这里来看的话,K进来,然后这个break这里是我们这个应该是进程啊,应该是进程结束的时候,而不是释放DL的时候。释放DL的时候,他应该不会进来。从现在测试的情况来看的话,应该是进程要结束的时候会不会触发。因为他只执行了一次啊,只有我们这个进程在退出的时候,它会触发这个事件。因为我们FreeLibrary的时候,你看他是没有备注发的,这个我们还是要以测试为准。我们可以测试两次,把它放到这里边。这里我们再测试一次,这个等待回升我们就说注释掉,重新再来一次。这个时候我们FreeLibrary是执行了两次,FreeLibrary会不会产生相应的事情,然后我们再仔细看一下。那么这里的话你看两次释放的话,都是成功的释放的结果,但是呢它只会只触发一次这个Dio Processor Touch,那么从这个情况来看的话,这个Dio Processor Touch来,我们应该也要改变它的一个定义,这个应该是我跳起来应该是什么呢,应该是应该是进程被创建时啊,或者是首次啊,或者解释为是什么呢,首次LoadLibrary是才会被处罚啊,才会出发。我们这里的话应该是首次这个LoadLibrary的时候,他会出发这个事件。如果是其他的那种其他的,比如说他是那种符号的套路的那种啊,就是我们的皮那个讲的有点远了,因为我们动态链接库呢它分为我们LoadLibrary它是动态的加载进来,另外还有一种静态的,就是它有一个那个PP1的,有个叫套路表的东西啊,如果套路表里面它用到了某个函数里,某个动态链接库里面的这个函数的话,它也会自动的在精神创建的时候去加载这个动态链接库啊,你家在哪。三次只是放了两次,看一下这三次两次的话,这个加载三次和两次应该是没有什么区别的,那么我们把它注释掉。因为它可以无限制地多次加载,第二次加载的话,它实际上只是给你返回这个地址而已。比如说我们把这行你把它去掉,应该也是同样的结果,因为我们还是要以测试为准的。看一下,这个时候呢被夹在要结束进程要结束时,好像是刚才是我的那个问题。那么这个时候我们在释放的时候,他那个跌落了processor or踏起来,这个事件被处罚了。那么刚才的话可能是我写的代码多写了一次,没有好,我们再来测试一下。啊,应该的确是刚才的这个问题,因为这个LoadLibrary的话,他可能加载的过程还没有加载完,那么我们这里去FreeLibrary的时候,他可能把他影响到了,给打断了是吧。我们再来看一下啊,如果把这一行代码注掉,注释掉之后呢,跟我们之前说的那就差不多了吧,也就是FreeLibrary会触发这个DIO Processor TG,而不是进程结束的时候。那么我们把它给备注一下就行,就是LoadLibrary加在什么,就正常情况下,它加载的时候会触发这个DIO Processor Touch,但是我们经过我们的测试了,我们也发现了,如果是连续两次去加载的话,他只有首次会触发啊。经过我们的测试,如果你是连续两次调用这个LoadLibrary,他只有首次触发的。另外这个呢,的确是我们FreeLibrary释放死释放LoadLibrary会被触发好,那就这两个。其他的一个是创建新线程是吧,我们县城相关的,我们还没有学。所以说这两个我们把它给注释掉,暂时不用去了解这两个。那今天呢我们先测试到这个地方,那么下期教程呢有函数没有那个技术可以查询技术应该是可以查询的,但是他是在内核里面的,我们是查询不到的。内核里边的话,它都有一个技术的某一个句柄被引用的次数啊,什么的都有技术。我们这里没有,在内核里面应该是有技术的,但是我们这里是直接查询不到的,那个超出本科的一个范围。暂时我们讲另外还有一个呢就是套出函数是吧,套出函数我们可以简单的说一下。比如说我们随便写一个套出函数写一个Test,因为动态链接库它最主要的作用就是套出函数一个共享的代码库。好随便写个函数啊,如果其他的嗯项目呢要使用到这个动态链接库,那么就可以通过Test t11这个符号来定位。但是要使用的话我们这里一般可以加一个模块啊,DEF的模块文件给他加进来,我找一下这里有个模块定义文件给他加进来。当然还有其他的办法,其他的办法那个代码不太好记。然后呢这里呢我们一般写一个ES,我看一下这个格式还有个格式,我们按照这个格式来啊,大写的EX HOT啊,下面的是函数名带编号,那么这个是一个什么意思呢啊,就套出套出的话,比如说TXT啊,这样呢我们的加载之后呢这个动态链接库呢它就套出了啊,Test但是我们是写在这里的就不行了,我看刚才写到哪里去了。这里有个TXT11,这里有TXT11才可要相同相同的,这个时候边缘才能够通过,然后通过了之后呢呃这个时候我们用调试器进去查看的时候就能看到我们看这个单独运行运行我们调试器了,才能够附加进去附加到那个控制台程序,我们看一下里面的符号,那么我们的符号那个TXT到了现在有没有加载一下已经被释放掉了,那个Test啊我们要改一下是吧,改一下,就这样的话他释放了我们是看不到的,看不到那个符号的。摇摇到这个符号,那么这个符号呢他另外他还还可以加这个编号加别名的啊,它有多种名字如果你加了编号本来他这个编号本来默认是唯一的顺序的你可以给他加其他的一个编号,比如说我们这里加二的平衡好,然后我们看一下在这里呢我们给他来一个GetChar如果不加这个GT下的话它加载之后马上就释放掉了我们的XD Bug来看他是看不到的。