python下通过ctypes使用原生C函数获取进程名(szExeFile)失败问题

#python下通过ctypes使用原生C函数获取进程名(szExeFile)失败问题
代码如下:

from ctypes.wintypes import DWORD

from ctypes import (
    Structure,
    c_char,
    sizeof,
    byref,
    windll,
)

PATH_MAX= c_char*265 #为什么我的不能小与265?别人是260

class structure(Structure): #参照C自制结构体
    _fields_ = [
        ('dwSize',DWORD),
        ('cntUsage',DWORD),
          ('th32ProcessID',DWORD),
          ('th32DefaultHeapID',DWORD),
          ('th32ModuleID',DWORD),
          ('cntThreads',DWORD),
          ('th32ParentProcessID',DWORD),
          ('pcPriClassBase',DWORD),
          ('dwFlags',DWORD),
          ('szExeFile',PATH_MAX) #注意这个值
        ]
def win32showprocess():
    CreateToolhelp32Snapshot=windll.kernel32.CreateToolhelp32Snapshot #获取进程快照
    store_struc=structure() 
    store_struc.dwSize=sizeof(structure) #初始化结构体
    kz_handle=CreateToolhelp32Snapshot(0x2,0) 
    if windll.kernel32.Process32First(kz_handle,byref(store_struc)):
        while windll.kernel32.Process32Next(kz_handle,byref(store_struc)):
            yield store_struc
processlist=win32showprocess()

#以下代码试图通过for循环获取进程名称,结果与预期不符,得到单个的char,似乎也不是首字母。不知道是什么。如下:

for processinfo in processlist:
    print('进程ID:%d\t进程名称:%s'%(processinfo.th32ProcessID,processinfo.szExeFile))

进程ID:4 进程名称:b'\x08'
进程ID:236 进程名称:b'\x08'

#我尝试使用psutil模块获取进程名称,结果如预期一致:

from psutil import Process
for processinfo in processlist:
    print('进程ID:%d\t进程名称:%s'%(processinfo.th32ProcessID,Process(processinfo.th32ProcessID).name()))

进程ID:4 进程名称:System
进程ID:236 进程名称:Registry

使用环境window10 python3.9.13
输出时szExeFile的类型为‘bytes’,所有长度均为1,怀疑是在szExeFile写入时就出了问题。
问题:第一个for的问题处在哪里?产生的原因是什么,怎么避免。

你应该看看 Process32First 参数 PROCESSENTRY32的原型

typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  CHAR      szExeFile[MAX_PATH];
} PROCESSENTRY32;

th32DefaultHeapID 的类型是ULONG_PTR,他表示一个指针长度的无符号整型,
也就是说如果你的 python是64位版本那么这个变量的长度就是8字节
python32位版本那么这个变量的长度就是4字节
那么你应该使用 c_void_p 

```python

from ctypes import c_void_p 

class structure(Structure):
    _fields_ = [
        ('dwSize',DWORD),
        ('cntUsage',DWORD),
          ('th32ProcessID',DWORD),
          ('th32DefaultHeapID', c_void_p ), #修改这里
          ('th32ModuleID',DWORD),
          ('cntThreads',DWORD),
          ('th32ParentProcessID',DWORD),
          ('pcPriClassBase',DWORD),
          ('dwFlags',DWORD),
          ('szExeFile', c_char *260 ) # winapi 宏定义 MAX_PATH 值是 260 
        ]

参考GPT和自己的思路,在获取进程名称时,processinfo.szExeFile返回的是一个字节数组(bytes),而不是一个字符串(str)。这就是为什么你看到的是一些乱码。

解决这个问题的方法是将字节数组转换为字符串。你可以使用 .decode() 方法将字节数组转换为字符串。例如,你可以在 for 循环中将这一行:

print('进程ID:%d\t进程名称:%s'%(processinfo.th32ProcessID,processinfo.szExeFile))

修改为:

print('进程ID:%d\t进程名称:%s'%(processinfo.th32ProcessID,processinfo.szExeFile.decode('gbk')))

如果你的操作系统是 Windows,这个代码应该可以正常工作。如果你的操作系统是其他的,你需要根据你的操作系统来修改 'gbk' 这个参数。

另外,你也可以通过修改 PATH_MAX 的值来让它小于 265。但是请注意,你需要确保你选择的大小足够容纳你要获取的进程名。如果你选择的大小太小,可能会截断进程名。
如果对您有帮助,请给与采纳,谢谢。

该回答引用ChatGPT

问题出在以下代码:


for processinfo in processlist:
    print('进程ID:%d\t进程名称:%s'%(processinfo.th32ProcessID,processinfo.szExeFile))

在 C 中,字符串是以 null 结尾的字符数组。在 structure 中,szExeFile 被定义为一个长度为 265 的字符数组。然而,对于从 Process32Next 返回的结构,szExeFile 字段包含的仅仅是 null 结尾的字符串中的第一个字符。因此,打印 processinfo.szExeFile 实际上是打印了字符串中的第一个字符。

为了获取完整的进程名称,需要使用 ctypes 的 create_string_buffer 方法创建一个字符串缓冲区,并将其传递给 Process32Next 函数:


from ctypes import create_string_buffer

def win32showprocess():
    CreateToolhelp32Snapshot = windll.kernel32.CreateToolhelp32Snapshot
    store_struc = structure() 
    store_struc.dwSize = sizeof(structure)
    kz_handle = CreateToolhelp32Snapshot(0x2, 0)
    if windll.kernel32.Process32First(kz_handle, byref(store_struc)):
        while windll.kernel32.Process32Next(kz_handle, byref(store_struc)):
            process_name = create_string_buffer(store_struc.szExeFile)
            yield store_struc.th32ProcessID, process_name.value.decode()

在上述代码中,使用 create_string_buffer 创建了一个缓冲区 process_name,然后将其传递给 Process32Next 函数。在循环中,使用 process_name.value 获取缓冲区中的数据,然后使用 decode() 将其转换为 Python 字符串。

以下答案基于ChatGPT与GISer Liu编写:

问题可能出在szExeFile的类型定义上。在PATH_MAX定义中,您使用了c_char * 265,因此您分配了一个长度为265的字符串。这应该足够大了,因为在Windows API中,最长的文件名是MAX_PATH,这个值是260。因此,你的字符串长度需要比260更大,以便能够容纳文件名。

在Windows API中,字符串以NULL结尾。因此,对于保存进程名称的字符串,您需要在每个字符串的结尾添加一个NULL终止符。这可以通过以下方式完成:

python

class structure(Structure): #参照C自制结构体
    _fields_ = [
        ('dwSize',DWORD),
        ('cntUsage',DWORD),
        ('th32ProcessID',DWORD),
        ('th32DefaultHeapID',DWORD),
        ('th32ModuleID',DWORD),
        ('cntThreads',DWORD),
        ('th32ParentProcessID',DWORD),
        ('pcPriClassBase',DWORD),
        ('dwFlags',DWORD),
        ('szExeFile', c_char * 265)
    ]

在这个结构体中,我们使用了c_char * 265来定义一个大小为265的字符串。由于这是一个字符串,我们需要在末尾添加一个NULL终止符,这将导致实际字符串长度为264。您可以在每个字符串的结尾添加'\0'(或b'\0')以添加NULL终止符,例如:

python

for processinfo in processlist:
    exe_name = processinfo.szExeFile.value.decode('utf-8').rstrip('\0')
    print('进程ID:%d\t进程名称:%s'%(processinfo.th32ProcessID, exe_name))

这里我们使用了szExeFile.value来获取C字符串的Python表示形式,并使用decode()将其转换为UTF-8字符串。rstrip()函数用于去除字符串末尾的NULL终止符。

这应该可以解决你的问题。


输出时szExeFile的类型为‘bytes’,所有长度均为1,怀疑是在szExeFile写入时就出了问题。如果其正常获取,即使无法正确打印,长度应该是大于1的

你的代码中出现了两个常见的错误,第一个是使用sizeof(structure)来初始化dwSize字段。因为Python的ctypes在生成结构体时会自动在结构体开头加上一个指向结构体类型的指针,这个指针的大小会在32位和64位系统上有所不同,因此用sizeof(structure)初始化dwSize字段可能会导致获取的进程名称不正确。正确的初始化方法是将dwSize字段设置为结构体的大小减去指针的大小,如下所示:

store_struc.dwSize = sizeof(structure) - sizeof(DWORD)

此外,在定义PATH_MAX时,你将字符串的长度设置为265。实际上,Windows API中定义的最大路径长度为260个字符,再加上终止符\0,所以你需要将PATH_MAX的长度设置为261。