这两天一直在搞cmd回显的问题,虽然已经搞定,但还是存在一些问题,这就是CreateProcess的问题
首先问题描述:
CreateProcess这个函数非常熟悉,再熟悉不过了 创建进程 具体说明如下:
BOOL CreateProcess(
LPCWSTR pszImageName,
LPCWSTR pszCmdLine,
LPSECURITY_ATTRIBUTES psaProcess,
LPSECURITY_ATTRIBUTES psaThread,
BOOL fInheritHandles,
DWORD fdwCreate,
LPVOID pvEnvironment,
LPWSTR pszCurDir,
LPSTARTUPINFOW psiStartInfo,
LPPROCESS_INFORMATION pProcInfo
);
主要就是其参数设置,其中有一半参数都可以设置为NULL,但又一些参数是非常重要的,下面就一一说明比较重要的参数:
第一个参数 LPCWSTR pszImageName specify the full path and filename of the module to execute or it can specify a partial path and filename
第二个参数 LPCWSTR pszCmdLine 命令行参数的设置
其次就是 BOOL fInheritHandles这个参数是表示此进程能否继承父进程的句柄 true表示能 false表示 不能
然后就是 LPSTARTUPINFOW psiStartInfo, LPPROCESS_INFORMATION pProcInfo 这两个参数 需要初始化
那简单的参数介绍完后,问题就出在第一个参数和第二个参数 在vs2005 MSDN中 这两个参数是这样说明的
The string can specify the full path and filename of the module to execute or it can specify a partial path and filename.
The lpszImageName parameter must be non-NULL and must include the module name.
也就是说必须是非空的,必须有一个执行文件
The system adds a null character to the command line, trimming the string if necessary, to indicate which file was used.
The lpszCmdLine parameter can be NULL. In that case, the function uses the string pointed to by lpszImageName as the command line.
If lpszImageName and lpszCmdLine are non-NULL, * lpszImageName specifies the module to execute, and * lpszCmdLine specifies the command line.
也就是说可以为空结论是在vs2005下创建进程CreateProcess的第一个参数必须非空
也就是说vs005下不能执行以下程序:
STARTUPINFO si={sizeof(si)};
PROCESS_INFORMATION pi;
CreateProcess(NULL,TEXT("NOTEPAD"),NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
上面这段程序是《windows 核心编程》里的代码,主要功能就是从命令行执行记事本NOTEPAD,为什么在vs2005下不能运行?不光是vs2005,还有 vs2008,vs2010,都存在这样的问题,那从命令行运行程序到底行不行,在这本书里也说到了一点,但说的有点深不是很理解,请继续往下看:
以下为收集资料仅供方便查看之用(转载的):
vs05里面 项目默认定义了 UNICODE 和 _UNICODE宏
在项目属性 -> C/C++ -> 预处理器 -> 预处理器定义里可以看到有从项目默认继承的UNICODE 和 _UNICODE宏
这导致 CreateProcess 调用的是 CreateProcessW
vc6里面 项目默认没有定义 UNICODE 和 _UNICODE宏
所以 CreateProcess 调用的是 CreateProcessA
而 CreateProcess 的第2个参数 lpCommandLine MSDN有如下说明:
The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.
CreateProcessW.会改变这个字符串的内容.因此,这个参数不能是一个指向只读内存的变量或者是字符串字面值.如果这个参数是一个常量字符串,函数有可能会造成存取违规.
LP 是 long pointer 的意思 (在32位机中 不再区分near pointer 和 long pointer) 就是说 是一个指针
C 是 constant 的意思 就是说是一个常量
W 是 Wide character的意思 表明是一个宽字符
STR 是 string 的意思 表示是一个字符串
T 是 Tchar 的意思 表示该类型会根据是否定义 _UNICODE UNICODE 被替换为 非W或者W类型
CreateProceesA 第2参数类型是 LPSTR 就是说一个指向 单字节字符串的指针 (现在Windows版本下 原型就是是 char *)
CreateProceesW 第2个参数类型是 LPWSTR 就是说是一个指向宽字节字符串的指针 (原型 wchar_t *)
CreateProcees 第2个参数类型是 LPTSTR 就是说 会根据 U宏 决定是 LPSTR 还是 LPTSTR (原型TCHAR*)
就是说 该函数将第2个参数视为可读写的 该函数不保证该字符串内容不被修改
而CreateProceesW 确实修改了其内容.... (关于如何修改的... 有个实验 呆会说....)
MessageBoxA 跟字符串有关的参数 都是 LPCSTR ( const char*)
MessageBoxW LPCWSTR (const wchar_t*)
MessageBox LPCTSTR (const TCHAR* )
该函数将这2个参数视为不可读写的 并且保证不会修改其内容
所以用 MessageBox(NULL,TEXT("Text"),TEXT("Caption"),0); 就不会有问题
最下面有一段代码将用一个线程 不断监视传给CreateProcess的第2个参数的内容
对CreateProcess的第2个参数 传入的是
TCHAR szCmd[] = TEXT("this is a cmd line");
首先是W版
以下是程序的输出
(每次输出内容都不一定相同 因为主线程和监视线程的调度情况不可预知 这次输出是最能反应实质的输出)
116 104 105 115 0 105 115 32 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 0 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 0 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 0 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 请按任意键继续. ..
联想到什么了吗?
传入的参数类容是 TEXT("this is a cmd line")
't'=116
'h'=104
'i'=105
's'=115
' '=32 (空格)
...
也就是说 在CreatProcessW执行过程中
监视线程执行了5次 并且检测到 "this is a cmd line" 中的4个空格被分别替换成了 '/0'=0
再联想 cmdline 被认作是一种以空格分隔的(space-delimited ) 不定项目的字符串
所以 CreatProcessW对第2个参数干的事情就是 依次将空格替换成 '/0'(字符串终结符)
this is a cmd line
在5次监视线程插足的时候CreatProcessW将第2参数分别被当作
this
this is
this is a
this is a cmd
this is a cmd line
5个字符串处理
当然 它们都不是合法的应用程序名称
(第1参数为NULL时 将把第2参数的第一个空格前——即是第一个项目 认作应用程序名称)
所以在本例中CreateProcessW 最终会失败
但是也反应出了问题本质~~
虽然函数执行完毕后 会恢复第2参数的内容 但是它曾经是被修改过的! 所以不能传入只读存储区地址!
如果去掉UNICODE _UNICODE宏定义 重新编译 程序输出如下
(同样 只是多次输出中的一次)
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 101
116 104 105 115 32 105 115 32 97 32 99 109 100 32 108 105 110 101 101 请按任意键
继续. . .
可以看出 CreateProcessA 没有对第2参数进行修改
以下是代码
#include <windows.h>
#include <process.h>
#include <tchar.h>
#include <stdio.h>
typedef struct tagUserData
{
const TCHAR * pctstr;
int len;
} UserData;
unsigned int __stdcall Monitor(void *pvParam);
int main()
{
STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.cb = sizeof(si);
//和楼主代码有点区别
//照理说应该在ZeroMemory之后
//设置 cb 为该结构体的大小
PROCESS_INFORMATION pi;
ZeroMemory(&pi,sizeof(pi));
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = TRUE;
TCHAR szCmd[] = TEXT("this is a cmd line");
UserData param;
param.pctstr = szCmd;
param.len = static_cast<int>( _tcslen(szCmd) );
// _tcslen 是 strlen 的"T" 版本
// 建立监视线程
HANDLE hThread =
reinterpret_cast<HANDLE> ( _beginthreadex(NULL,0,Monitor,static_cast<void*>(& param),0,NULL) );
//调高其优先权 利于监视
SetThreadPriority(hThread,THREAD_PRIORITY_HIGHEST);
BOOL bRet = ::CreateProcess (
NULL,
szCmd,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi);
return 0;
}
unsigned int __stdcall Monitor(void *pvParam)
{
for (;;)
{
UserData *pData = static_cast<UserData*>(pvParam);
for (int i=0;i<pData->len;++i)
_tprintf(TEXT("%d "),pData->pctstr[ i ] );
_tprintf(TEXT("/n"));
}
return 0;
}
问题得以解决,总结一下,关键还是字符集的问题,哈哈 ,从内部来讲,CreateProcess会修改传递给它的命令行字符串,如果命令行字符串包含在文件映像的只读部分,那么函数就会出现违规访问的问题,例如上文在《windows核心编程》中的那段代码就是典型的例子。当CreateProcess函数试图修改这个字符串时就会发生违规访问(较早的Visual C++版本中将字符串放入读/写内存,因此调用CreateProcess不会出现违规访问)。解决这个问题的最佳方法是,在调用CreateProcess之前,将常量字符串复制到临时缓存中,程序代码如下:
STARTUPINFO si={sizeof(si)};
PROCESS_INFORMATION pi;
TCHAR cmd[]=TEXT("NOTEPAD");
CreateProcess(NULL,cmd,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
就这么简单,也可以使用Visual C++的/GF和/GF编译器开关,这些开关可以控制重复字符串的删除,并确定是否将这些字符串放入只读内存部分(注意,/ZI开关包含了/GF开关的功能,允许使用Visual Studio的Edit&Continue调试特性)。最好是使用/GF编译器开关和临时缓存。Microsoft公司能够做到的是设置好CreateProcess,让函数来生成一个字符串的临时副本,简化我们的工作。调用windows 2000以上版本的系统上CreateProcess的ANSI版本不会产生违规访问(即CreateProcessA),因为调用一个函数,系统就已经生成了一个命令行字符串的历史副本。详细的请看《windows核心编程》。
但太深的原理还不是很理解,需要以后在学习中继续摸索。如果有哪位牛人对此比较熟悉,请不吝赐教。在下不胜感激