由于我的需求是短时间内ping多台机子,所以需要异步执行,微软提供的例子是同步方式的,根据微软官方提供的icmpSendEcho2 函数的信息
,我需要定义一个空的宏PIO_APC_ROUTINE_DEFINED ,定义完之后,编译又出现“未声明的标识符”,最后上网查需要定义两个数据类型。
typedef struct _IO_STATUS_BLOCK
{
union {
long Status;
void * Pointer;
};
unsigned long *Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;
typedef void (__stdcall* PIO_APC_ROUTINE) (
void* ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
unsigned long Reserved
);
这样就可以编译通过了,但需要注意的是你最后需要调用SleepEx,你设置的回调函数才会被执行。
下列代码根据微软官方例子改编
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define PIO_APC_ROUTINE_DEFINED//必须要添加在对应的头文件之前,或者在VS解决方案的属性->C/C++->预处理器里添加
typedef
struct _IO_STATUS_BLOCK {
union {
long Status;
void* Pointer;
};
unsigned long* Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;
typedef
void
(__stdcall* PIO_APC_ROUTINE) (
void* ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
unsigned long Reserved
);
#include <winsock2.h>
#include <iphlpapi.h>
#include <icmpapi.h>
#include <stdio.h>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
int CALLBACK ReplyCame(PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, ULONG Reserved)
{
//char* szAddr = (char*)ApcContext;//可以不转,直接用ApcContext去打印
printf("Replay Came for %s...... \n", ApcContext);
g_bReply = TRUE;
return 0;
}
int __cdecl main(int argc, char** argv)
{
HANDLE hIcmpFile;
unsigned long ipaddr = INADDR_NONE;
DWORD dwRetVal = 0;
DWORD dwError = 0;
char SendData[] = "Data Buffer";
LPVOID ReplyBuffer = NULL;
DWORD ReplySize = 0;
char csIP[] = "192.168.1.103";
ipaddr = inet_addr(csIP);
if (ipaddr == INADDR_NONE) {
printf("usage: %s IP address\n", csIP);
return 1;
}
hIcmpFile = IcmpCreateFile();
if (hIcmpFile == INVALID_HANDLE_VALUE) {
printf("\tUnable to open handle.\n");
printf("IcmpCreatefile returned error: %ld\n", GetLastError());
return 1;
}
// Allocate space for at a single reply
ReplySize = sizeof(ICMP_ECHO_REPLY) + sizeof(SendData) + 8;
ReplyBuffer = (VOID*)malloc(ReplySize);
if (ReplyBuffer == NULL) {
printf("\tUnable to allocate memory for reply buffer\n");
return 1;
}
dwRetVal = IcmpSendEcho2(hIcmpFile, NULL, (PIO_APC_ROUTINE)ReplyCame, NULL,
ipaddr, SendData, sizeof(SendData), NULL,
ReplyBuffer, ReplySize, 1000);
if (dwRetVal != 0) {
PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer;
struct in_addr ReplyAddr;
ReplyAddr.S_un.S_addr = pEchoReply->Address;
printf("\tSent icmp message to %s\n", argv[1]);
if (dwRetVal > 1) {
printf("\tReceived %ld icmp message responses\n", dwRetVal);
printf("\tInformation from the first response:\n");
}
else {
printf("\tReceived %ld icmp message response\n", dwRetVal);
printf("\tInformation from this response:\n");
}
printf("\t Received from %s\n", inet_ntoa(ReplyAddr));
printf("\t Status = %ld ", pEchoReply->Status);
switch (pEchoReply->Status) {
case IP_DEST_HOST_UNREACHABLE:
printf("(Destination host was unreachable)\n");
break;
case IP_DEST_NET_UNREACHABLE:
printf("(Destination Network was unreachable)\n");
break;
case IP_REQ_TIMED_OUT:
printf("(Request timed out)\n");
break;
default:
printf("\n");
break;
}
printf("\t Roundtrip time = %ld milliseconds\n",
pEchoReply->RoundTripTime);
}
else {
dwError = GetLastError();
if (dwError != ERROR_IO_PENDING)//调用IcmpSendEcho2使用回调的方式时,返回ERROR_IO_PENDING是正常的结果,并不是错误
{
printf("Call to IcmpSendEcho2 failed.\n");
switch (dwError) {
case IP_BUF_TOO_SMALL:
printf("\tReplyBufferSize too small\n");
break;
case IP_REQ_TIMED_OUT:
printf("\tRequest timed out\n");
break;
default:
printf("\tExtended error returned: %ld\n", dwError);
break;
}
return 1;
}
}
SleepEx(1000, TRUE);//没这行代码回调不会被执行
printf("全部处理完成。\n");
system("pause");
return 0;
}
遇到的问题和解决方式:
- 程序一回调就崩溃
编译没问题,回调也调用的,但调用完就崩,崩在了不知名的位置。后来上网查了,看到有说是__stdcall和__cdecl的问题。因为汇编是通过入栈出栈的方式调用函数的,在调用函数前,把参数压入栈,执行call指令才会跳到函数体执行,这时函数自己再通过出栈的方式使用参数,而__stdcall和__cdecl的区别就是在函数执行完后栈顶的处理方式不同。C/C++的函数默认是__cdecl。
__cdecl需要在函数返回之后,根据之前压栈的参数去调整栈顶。如下图
__stdcall是函数内部自己根据参数调整栈顶。如下图
也就说遵守__cdecl规定的人A 去写汇编,它写的函数并不会在ret时调整栈顶,它会在call指令之后,加一条指令去调整栈顶,让栈顶恢复到调用函数之前。而遵守__stdcall规定的人B 去写汇编,它就会在自己的函数里面调整栈顶,让栈顶恢复到调用函数之前。而当着两种规定的人A B一起写代码的时候,当B用call指令去调用A的函数时,由于B默认就A的函数是自己处理去调整栈顶的,实际上A并没有,所以当函数返回的时候,栈顶可能已经不是调用函数之前的栈顶了,如果后续从栈顶拿数据进行操作的时候,可能就因为操作非法地址崩了。所以现在要做的就是让它ret之后,栈顶能恢复到调用之前就行了,用什么方式都无所谓。
由于我们使用的不是源文件,而是库,所以它的指令已经是固定的了,如果它是B去写的汇编,那我们就要按B的方式去写函数,也就是函数声明要加__stdcall,同时函数的参数也要保证ret后,栈顶能恢复到调用之前就行了。
VS有个调试功能叫反汇编,需要看汇编代码的,可以在调试->窗口->反汇编,但需要代码编译成功并运行的时候才会有。
- 未定义标识符PIO_APC_ROUTINE
在微软官方文档中没有找到PIO_APC_ROUTINE的定义,这定义也是在别人帖子上看到的。根据我们前面讲的,我们知道只要函数ret之后,栈顶能恢复到call之前就行,所以你怎么定义这个PIO_APC_ROUTINE都行,只要能让栈顶恢复就不会崩,甚至能把参数缩短到两个(4个字节+8个字节),因为它需要将堆栈调整0CH字节。将参数强转,也可以根据你的想法去用。 - 回调不执行
它的异步不是完成了就会去执行回调函数,而是需要你用SleepEx函数才会执行回调(目前只尝试到这种方式可以),而且IcmpSendEcho2和SleepEx需要在同一个线程里。所以,如果你想它执行回调,同时又不想阻塞在某个位置,那么只能用其他线程去执行IcmpSendEcho2和SleepEx