最近接手一个小项目,要求使用谷歌的aapt.exe获取apk软件包中的信息。依稀记得去年年中时,有个同事也问过我如何获取被调用进程的输出结果,当时还研究了一番,只是没有做整理。今天花点时间,将该方法整理成文。(转载请指明出于breaksoftware的csdn博客)
在信息化非常发达的今天,可能已经过了江湖“武侠”草莽的时代。仅凭一己之力想完成惊人的创举,可谓难上加难。于是社会分工越来越明确:你擅长写驱动,你就去封装个驱动出来;他擅长写界面,就让他写套界面出来。如果你非常好心,可以将自己的研究成果开源,那么可能会有千万人受益。如果你想保持神秘感,但是还是希望别人可以分享你的成果,你可能会将模块封装出来供别人使用。比如你提供了一个DLL文件和调用方法样例。但是,实际情况并不是我们想的那么简单。比如我文前提到的问题:别人提供了一个Console控制台程序,我们将如何获取其执行的输出结果呢?这个问题,从微软以为为我们考虑过了,我们可以从一个API中可以找到一些端倪——CreateProcess。
BOOL WINAPI CreateProcess(
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
做Windows开发的同学对CreateProcess这个API应该非常眼熟,也应该经常调用过。但是仔细研究过这个API每个参数的同学应该不会太多吧。这个API的参数非常多,我想我们工程中对CreateProcess的调用可能就关注于程序路径(lpApplicationName),或者命令行(lpCommandLine)。而其他参数我们可能就保守的选择了NULL。(遥想2年前,我就是在这个API上栽了一个大大的跟头。)
本文,我们将关注一个可能很少使用的参数lpStartupInfo。它是我们启动子进程时,控制子进程启动方式的参数。其结构体是STARTUPINFO
typedef struct _STARTUPINFO {
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
BOOL ExecDosCmd(const CString& cstrCmd, char** ppBuffer)
{
HANDLE hRead = NULL;
HANDLE hWrite = NULL;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
// 新创建的进程继承管道读写句柄
sa.bInheritHandle = TRUE;
if ( FALSE == CreatePipe( &hRead, &hWrite, &sa, 0 ) ) {
return FALSE;
}
if ( NULL == hRead || NULL == hWrite ) {
return FALSE;
}
这儿我们创建一个管道,该管道提供两个句柄:hRead和hWrite。我们之后将hWrite交给我们创建的子进程,让它去将信息写入管道。而我们父进程,则使用hRead去读取子进程写入管道的内容。此处要注意的就是将SECURITY_ATTRIBUTES对象的bInheritHandle设置为TRUE,这样我们获取的两个操作管道的句柄就有可继承属性。为什么需要可继承属性,我们会在之后说明。
// 组装命令
CString cstrNewDosCmd = L"Cmd.exe /C ";
cstrNewDosCmd += cstrCmd;
// 设置启动程序属性,将
STARTUPINFO si;
si.cb = sizeof(STARTUPINFO);
GetStartupInfo(&si);
si.hStdError = hWrite; // 把创建进程的标准错误输出重定向到管道输入
si.hStdOutput = hWrite; // 把创建进程的标准输出重定向到管道输入
si.wShowWindow = SW_HIDE;
// STARTF_USESHOWWINDOW:The wShowWindow member contains additional information.
// STARTF_USESTDHANDLES:The hStdInput, hStdOutput, and hStdError members contain additional information.
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
PROCESS_INFORMATION pi;
// 启动进程
BOOL bSuc = CreateProcess(NULL, cstrNewDosCmd.GetBuffer(), NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi);
cstrNewDosCmd.ReleaseBuffer();
此处我们要注意几个点:
- “Cmd..exe /C” 我们使用CMD运行我们代理的程序。注意,我们启动的是CMD,而不是我们传入的文件路径。关于CMD命令的说明如下:
- 设置标准输出和标准错误输出句柄
si.hStdError = hWrite; // 把创建进程的标准错误输出重定向到管道输入
si.hStdOutput = hWrite; // 把创建进程的标准输出重定向到管道输入
- 隐藏CMD控制台
si.wShowWindow = SW_HIDE;
- 设置有效属性
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
这两个有效属性要设置。我们设置STARTF_USESHOWWINDOW的原因是:我们要控制CMD窗口不出现,所以我们修改了wShowWindow属性。我们使用STARTF_USESTDHANDLES的原因是:我们使用了标准输出和标准错误输出句柄。此处我们还要特别将一下STARTF_USESTDHANDLES属性的说明,我们看MSDN有如下描述
If this flag is specified when calling one of the process creation functions, the handles must be inheritable and the function's bInheritHandles parameter must be set to TRUE.
也就是说,我们设置的这些句柄要有可继承性。这就解释了我们之前为什么在创建管道时要将句柄可继承性设置为TRUE的原因。
一般来说,我们要代理的程序已经输入好信息了。我们要关闭写管道
if ( NULL != hWrite ) {
CloseHandle(hWrite);
hWrite = NULL;
}
之后便是读取管道信息。我想应该有人借用过网上相似的代码,但是却发现一个问题,就是读取出来的信息是不全的。这个问题的关键就在读取的方法上,其实没什么玄妙,只要控制好读取起始位置就行了。
// 先分配读取的数据空间
DWORD dwTotalSize = NEWBUFFERSIZE; // 总空间
char* pchReadBuffer = new char[dwTotalSize];
memset(pchReadBuffer, 0, NEWBUFFERSIZE);
DWORD dwFreeSize = dwTotalSize; // 闲置空间
do {
if ( FALSE == bSuc ) {
break;
}
// 重置成功标志,之后要视读取是否成功来决定
bSuc = FALSE;
char chTmpReadBuffer[NEWBUFFERSIZE] = {0};
DWORD dwbytesRead = 0;
// 用于控制读取偏移
OVERLAPPED Overlapped;
memset(&Overlapped, 0, sizeof(OVERLAPPED) );
while (true) {
// 清空缓存
memset(chTmpReadBuffer, 0, NEWBUFFERSIZE);
// 读取管道
BOOL bRead = ReadFile( hRead, chTmpReadBuffer, NEWBUFFERSIZE, &dwbytesRead, &Overlapped );
DWORD dwLastError = GetLastError();
if ( bRead ) {
if ( dwFreeSize >= dwbytesRead ) {
// 空闲空间足够的情况下,将读取的信息拷贝到剩下的空间中
memcpy_s( pchReadBuffer + Overlapped.Offset, dwFreeSize, chTmpReadBuffer, dwbytesRead );
// 重新计算新空间的空闲空间
dwFreeSize -= dwbytesRead;
}
else {
// 计算要申请的空间大小
DWORD dwAddSize = ( 1 + dwbytesRead / NEWBUFFERSIZE ) * NEWBUFFERSIZE;
// 计算新空间大小
DWORD dwNewTotalSize = dwTotalSize + dwAddSize;
// 计算新空间的空闲大小
dwFreeSize += dwAddSize;
// 新分配合适大小的空间
char* pTempBuffer = new char[dwNewTotalSize];
// 清空新分配的空间
memset( pTempBuffer, 0, dwNewTotalSize );
// 将原空间数据拷贝过来
memcpy_s( pTempBuffer, dwNewTotalSize, pchReadBuffer, dwTotalSize );
// 保存新的空间大小
dwTotalSize = dwNewTotalSize;
// 将读取的信息保存到新的空间中
memcpy_s( pTempBuffer + Overlapped.Offset, dwFreeSize, chTmpReadBuffer, dwbytesRead );
// 重新计算新空间的空闲空间
dwFreeSize -= dwbytesRead;
// 将原空间释放掉
delete [] pchReadBuffer;
// 将原空间指针指向新空间地址
pchReadBuffer = pTempBuffer;
}
// 读取成功,则继续读取,设置偏移
Overlapped.Offset += dwbytesRead;
}
else{
if ( ERROR_BROKEN_PIPE == dwLastError ) {
bSuc = TRUE;
}
break;
}
}
} while (0);
因为读取的信息量是不确定的,所以我段代码动态申请了一段内存,并根据实际读取出来的结果动态调整这块内存的大小。这段注释写的很清楚了,我就不再赘述。
善始善终,最后代码处理是
if ( NULL != hRead ) {
CloseHandle(hRead);
hRead = NULL;
}
if ( bSuc ) {
*ppBuffer = pchReadBuffer;
}
else {
delete [] pchReadBuffer;
pchReadBuffer = NULL;
}
return bSuc;
}
这个函数传入了一个指向指针的指针用于外部获取结果,外部一定要释放这段空间以免造成内存泄露。
#define NEWBUFFERSIZE 0x100
#define EXECDOSCMD L"aapt.exe"
int _tmain(int argc, _TCHAR* argv[])
{
char* pBuffer = NULL;
WCHAR wchFilePath[MAX_PATH] = {0};
DWORD dwSize = MAX_PATH - 1;
if ( FALSE == GetModuleFileName(NULL, wchFilePath, dwSize) ) {
return -1;
}
CString cstrFilePath = wchFilePath;
int nIndex = cstrFilePath.ReverseFind('\\');
if ( nIndex == -1 ) {
return -1;
}
cstrFilePath = cstrFilePath.Left(nIndex + 1);
cstrFilePath += EXECDOSCMD;
cstrFilePath += L"\"";
cstrFilePath = L"\"" + cstrFilePath;
if ( ExecDosCmd( cstrFilePath, &pBuffer ) &&
NULL != pBuffer ) {
CString cstrBuffer = CA2W(pBuffer, CP_UTF8);
delete [] pBuffer;
wprintf(L"%s", cstrBuffer);
}
return 0;
}
这样,我们就可以拿到子进程输出结果并加以分析。我这儿简单处理了下,就输出来。也算善始善终吧。
附上工程。