内联汇编代码以获取CPU ID

我在这里找到了一段很好的代码,它使用API​​调用执行ASM指令,以获取CPU的序列号:

using System; using System.Text; using System.Runtime.InteropServices; namespace ConsoleApplication1 { class Program { [DllImport("user32", EntryPoint = "CallWindowProcW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)] private static extern IntPtr ExecuteNativeCode([In] byte[] bytes, IntPtr hWnd, int msg, [In, Out] byte[] wParam, IntPtr lParam); [return: MarshalAs(UnmanagedType.Bool)] [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool VirtualProtect([In] byte[] bytes, IntPtr size, int newProtect, out int oldProtect); const int PAGE_EXECUTE_READWRITE = 0x40; static void Main(string[] args) { string s = CPU32_SerialNumber(); Console.WriteLine("CPU Serial-Number: " + s); Console.ReadLine(); } private static string CPU32_SerialNumber() { byte[] sn = new byte[12]; if (!ExecuteCode32(ref sn)) return "ND"; return string.Format("{0}{1}{2}", BitConverter.ToUInt32(sn, 0).ToString("X"), BitConverter.ToUInt32(sn, 4).ToString("X"), BitConverter.ToUInt32(sn, 8).ToString("X")); } private static bool ExecuteCode32(ref byte[] result) { // CPU 32bit SerialNumber -> asm x86 from c# (c) 2003-2011 Cantelmo Software // 55 PUSH EBP // 8BEC MOV EBP,ESP // 8B7D 10 MOV EDI,DWORD PTR SS:[EBP+10] // 6A 02 PUSH 2 // 58 POP EAX // 0FA2 CPUID // 891F MOV DWORD PTR DS:[EDI],EBX // 894F 04 MOV DWORD PTR DS:[EDI+4],ECX // 8957 08 MOV DWORD PTR DS:[EDI+8],EDX // 8BE5 MOV ESP,EBP // 5D POP EBP // C2 1000 RETN 10 int num; byte[] code_32bit = new byte[] { 0x55, 0x8b, 0xec, 0x8b, 0x7d, 0x10, 0x6a, 2, 0x58, 15, 0xa2, 0x89, 0x1f, 0x89, 0x4f, 4, 0x89, 0x57, 8, 0x8b, 0xe5, 0x5d, 0xc2, 0x10, 0 }; IntPtr ptr = new IntPtr(code_32bit.Length); if (!VirtualProtect(code_32bit, ptr, PAGE_EXECUTE_READWRITE, out num)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); ptr = new IntPtr(result.Length); return (ExecuteNativeCode(code_32bit, IntPtr.Zero, 0, result, ptr) != IntPtr.Zero); } } } 

我测试了它,它对我来说很好。 但我仍然有一些问题和问题:

1)我想在可以在x86和x64环境中运行的应用程序中实现此代码。 如果我将此代码运行到64x环境中,我会收到AccessViolationException。 该代码的作者说,这可以很容易地实现包含x64指令(RAX,RBX,RCX,RDX,…)的字节码数组。 我的问题是我绝对不知道如何将86x字节代码转换为x64字节代码,我甚至不知道ASM。 是否有可以执行此操作的转换表或实用程序?

2)此代码段是否适用于任何类型的处理器? 我在使用英特尔核心的笔记本电脑上进行了测试,但它可以工作……但是AMD呢?

3)我不确定我获得的值是否正确。 如果我运行以下代码:

 string cpuInfo = String.Empty; System.Management.ManagementClass mc = new System.Management.ManagementClass("Win32_Processor"); System.Management.ManagementObjectCollection moc = mc.GetInstances(); foreach (System.Management.ManagementObject mo in moc) { if (cpuInfo == String.Empty) cpuInfo = mo.Properties["ProcessorId"].Value.ToString(); } 

我得到的结果是“BFEBFBFF000306A9”。 代码段的结果是“F0B2FF0CA0000”。 为什么? 哪一个是正确的?

这是您修改的代码,以便在x64 x86上获得与Win32_Processor.ProcessorId相同的结果:

 using System; using System.Text; using System.Runtime.InteropServices; namespace ConsoleApplication1 { class Program { [DllImport("user32", EntryPoint = "CallWindowProcW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)] private static extern IntPtr CallWindowProcW([In] byte[] bytes, IntPtr hWnd, int msg, [In, Out] byte[] wParam, IntPtr lParam); [return: MarshalAs(UnmanagedType.Bool)] [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool VirtualProtect([In] byte[] bytes, IntPtr size, int newProtect, out int oldProtect); const int PAGE_EXECUTE_READWRITE = 0x40; static void Main(string[] args) { string s = ProcessorId(); Console.WriteLine("ProcessorId: " + s); Console.ReadLine(); } private static string ProcessorId() { byte[] sn = new byte[8]; if (!ExecuteCode(ref sn)) return "ND"; return string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8")); } private static bool ExecuteCode(ref byte[] result) { int num; /* The opcodes below implement a C function with the signature: * __stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam); * with wParam interpreted as a pointer pointing to an 8 byte unsigned character buffer. * */ byte[] code_x86 = new byte[] { 0x55, /* push ebp */ 0x89, 0xe5, /* mov ebp, esp */ 0x57, /* push edi */ 0x8b, 0x7d, 0x10, /* mov edi, [ebp+0x10] */ 0x6a, 0x01, /* push 0x1 */ 0x58, /* pop eax */ 0x53, /* push ebx */ 0x0f, 0xa2, /* cpuid */ 0x89, 0x07, /* mov [edi], eax */ 0x89, 0x57, 0x04, /* mov [edi+0x4], edx */ 0x5b, /* pop ebx */ 0x5f, /* pop edi */ 0x89, 0xec, /* mov esp, ebp */ 0x5d, /* pop ebp */ 0xc2, 0x10, 0x00, /* ret 0x10 */ }; byte[] code_x64 = new byte[] { 0x53, /* push rbx */ 0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, /* mov rax, 0x1 */ 0x0f, 0xa2, /* cpuid */ 0x41, 0x89, 0x00, /* mov [r8], eax */ 0x41, 0x89, 0x50, 0x04, /* mov [r8+0x4], edx */ 0x5b, /* pop rbx */ 0xc3, /* ret */ }; ref byte[] code; if (IsX64Process()) code = ref code_x64; else code = ref code_x86; IntPtr ptr = new IntPtr(code.Length); if (!VirtualProtect(code, ptr, PAGE_EXECUTE_READWRITE, out num)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); ptr = new IntPtr(result.Length); return (CallWindowProcW(code, IntPtr.Zero, 0, result, ptr) != IntPtr.Zero); } private static bool IsX64Process() { return IntPtr.Size == 8; } } } 

我在没有编译代码的情况下对C#部分进行了微不足道的修改(我目前没有安装Windows开发机器)所以如果有语法错误,请进行明显的修复。

我想强调一个非常重要的一点: 原始代码回读的内容不是 CPU序列号

  • 您使用了CPUIDfunction2(在执行CPUID指令之前将2放在EAX中 )。 如果您阅读了Intel和AMD CPUID应用程序说明,​​您将看到这会回读缓存和TLB硬件配置,并且仅在Intel上支持。
  • 我修改了你的代码以使用CPUID函数1,它读回CPU的步进,模型和系列。 这与WIN32_Processor.ProcessorID的行为相匹配
  • 现代x86 CPU没有一个序列号,这个序列号在“滚出assembly线”的其他相同单元中是唯一的。 处理器序列号仅在Pentium 3上通过CPUIDfunction3提供。

我现在将解释我使用的过程和工具。

将操作数组粘贴到Python脚本中,然后将操作码写入二进制文件( cpuid-x86.bin ):

 cpuid_opcodes = [ 0x55, 0x8b, 0xec, 0x8b, ... ] open('cpuid-x86.bin', 'w').write(''.join(chr(x) for x in cpuid_opcodes)) 

反汇编cpuid-x86.bin 。 我使用了udis86的udcli 。

 $ udcli -att cpuid-x86.bin 0000000000000000 55 push %ebp 0000000000000001 8bec mov %esp, %ebp 0000000000000003 8b7d10 mov 0x10(%ebp), %edi 0000000000000006 6a02 push $0x2 0000000000000008 58 pop %eax 0000000000000009 0fa2 cpuid 000000000000000b 891f mov %ebx, (%edi) 000000000000000d 894f04 mov %ecx, 0x4(%edi) 0000000000000010 895708 mov %edx, 0x8(%edi) 0000000000000013 8be5 mov %ebp, %esp 0000000000000015 5d pop %ebp 0000000000000016 c21000 ret $0x10 

立即脱颖而出的一件事就是当一个简单的“ mov $ 0x2,%eax ”会用“ push $ 0x2; pop%eax ”将值2移动到EAX时为什么会这样做?

我的猜测是“ push $ 0x2 ”, 6a02的指令编码更容易以hexforms修改。 手动和编程。 我想有人试图使用CPUID函数3获取处理器序列号,发现它不支持然后切换到使用function2。

最后“ ret $ 0x10 ”也不寻常。 RET指令的RET IMM16forms返回给调用者,然后从栈中弹出IMM16字节。 在函数返回后,被调用者负责从堆栈中弹出参数的事实意味着这不是使用标准的x86调用约定。

实际上,快速查看C#代码可以发现它正在使用CallWindowProc()来调用汇编函数。 CallWindowProc()的文档显示汇编代码正在实现具有以下签名的C函数:

 __stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam); 

__stdcall是32位Windows API使用的特殊函数调用约定。

汇编代码使用0x10(%ebp)作为函数的第三个参数,作为存储CPUID指令输出的字符数组。 (在x86上的标准函数序言之后, 8(%ebp)是第一个参数.0xc(%ebp)是第二个4字节参数, 0x10(%ebp)是第三个参数)我们的窗口过程函数原型中的第三个参数以上是wParam 。 它用作out参数,是汇编代码中使用的唯一参数。

关于汇编代码的最后一个有趣的事情是它破坏了寄存器EDIEBX而没有保存它们,违反了__stdcall调用约定。 当通过CallWindowProc()调用函数时,这个bug显然是潜在的,但是如果你试图在C中编写自己的main函数来测试汇编代码( cpuid-main.c ),它就会显示出来:

 #include  #include  void __stdcall cpuid_wind_proc(uint32_t hWnd, uint32_t msg, uint8_t *wparam, uint32_t lparam); enum { RESULT_SIZE = 2 * 4, /* Two 32-bit registers: EAX, EDX */ }; static unsigned int form_word_le(uint8_t a[]) { return (a[3] << 24) | (a[2] << 16) | (a[1] << 8) | a[0]; } int main() { uint8_t r[RESULT_SIZE]; memset(r, 0, sizeof(r)); cpuid_wind_proc(0, 0, r, 0); printf("%08x%08x\n", form_word_le(r + 4), form_word_le(r)); return 0; } 

固定以保存和恢复EDIEBX并使用CPUIDfunction1的程序集版本如下所示:

  .section .text .global _cpuid_wind_proc@16 _cpuid_wind_proc@16: push %ebp mov %esp, %ebp push %edi mov 16(%ebp), %edi push $1 pop %eax push %ebx cpuid mov %eax, (%edi) mov %edx, 0x4(%edi) pop %ebx pop %edi mov %ebp, %esp pop %ebp ret $16 

符号名称_cpuid_wind_proc @ 16是在32位Windows上修改__stdcall函数名称的方式。 @ 16是参数占用的字节数。 (四个参数,每个在32位Windows上占用四个字节,总计16个)

现在我准备将代码移植到x64了。

  • 通过查阅这个方便的ABI表,我看到前四个参数在RCXRDXR8R9中传递,因此wParamR8中
  • 英特尔文档告诉我CPUID指令破坏了EAXEBXECXEDXEBXRBX的下半部分,它是ABI中保存的GPR(“保存的GPR”在这里表示通用寄存器,应该在函数调用中保留其内容)所以我确保在执行CPUID指令和恢复之前保存RBX RBX之后。

这是x64程序集:

  .section .text .global cpuid_wind_proc cpuid_wind_proc: push %rbx mov $1, %rax cpuid movl %eax, (%r8) movl %edx, 4(%r8) pop %rbx ret 

正如您所看到的,x64版本更短,更容易编写。 在x64上只有一个函数调用约定,所以我们不必担心__stdcall

cpuid-main.c一起构建x64汇编函数,并将其输出与此VBScript( cpuid.vbs )进行比较:

 Set objProc = GetObject("winmgmts:root\cimv2:Win32_Processor='cpu0'") WScript.echo objProc.ProcessorId 

cpuid.vbs运行

 wscript cpuid.vbs 

并validation输出是否匹配。 (我实际上在Linux上使用MinGW-w64进行交叉编译,并在执行C和汇编工作的同时在Wine64仿真下运行程序。)

随着x64汇编CPUIDfunction的运行,我现在准备将代码集成回C#。

  • 反汇编cpuid-x64.exe以获取操作码并将其粘贴为新的字节数组( code_x64 )。
  • 通过在IsX64Process()中测试IntPtr.Size == 8来更改ExecuteCode()以确定是否运行x86或x64版本的CPUID代码。

最后,更改ProcessorId()以生成hex字符串:

 string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8")); 

使用“X8”而不是“X”确保将UInt32格式化为8位hex值,零填充。 否则,当您将它们连接成单个字符串时,您无法分辨哪些数字来自EDX以及哪些来自EAX

就是这样。

您发布的代码似乎调用了CPUID函数#2(由EAX寄存器给出,在PUSH 2; POP EAX )。 根据不用于查询序列号的intel指令集引用:

当CPUID在EAX设置为2的情况下执行时,处理器将返回有关EAX,EBX,ECX和EDX寄存器中处理器内部TLB,高速缓存和预取硬件的信息。

另请注意,此function在AMD处理器上不可用,但代码仍然可以正确执行。