Here, your code has been modified to get the same result as Win32_Processor.ProcessorId on both x64 and x86:
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; byte[] code_x86 = new byte[] { 0x55, 0x89, 0xe5, 0x57, 0x8b, 0x7d, 0x10, 0x6a, 0x01, 0x58, 0x53, 0x0f, 0xa2, 0x89, 0x07, 0x89, 0x57, 0x04, 0x5b, 0x5f, 0x89, 0xec, 0x5d, 0xc2, 0x10, 0x00, }; byte[] code_x64 = new byte[] { 0x53, 0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x0f, 0xa2, 0x41, 0x89, 0x00, 0x41, 0x89, 0x50, 0x04, 0x5b, 0xc3, }; 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; } } }
I made trivial changes for the C # part without compiling the code (I don't have Windows dev installed at the moment), so if there are syntax errors, just make the obvious fix.
I want to emphasize one very important point: what your source code read was not the processor serial number :
- You used the CPUID 2 function (by placing 2 in EAX before executing the CPUID command ). If you read Intel and AMD CPUID in the application notes, you will see that it reads the TLB cache and hardware configuration and is only supported on Intel.
- I modified your code to use the CPUID 1 function, which reads the step, model and CPU family. This corresponds to the behavior of Win32_Processor.ProcessorId
- Modern x86 processors do not have a serial number that is unique among other identical units that roll the conveyor. The processor serial numbers were only available for Pentium 3 through the CPUID 3 feature.
Now I will explain the process and tools that I used.
Insert an array of opcodes into a Python script that will then write the opcodes in a binary file ( cpuid-x86.bin ):
cpuid_opcodes = [ 0x55, 0x8b, 0xec, 0x8b, ... ] open('cpuid-x86.bin', 'w').write(''.join(chr(x) for x in cpuid_opcodes))
Take apart cpuid-x86.bin . I used udcli from udis86 .
$ 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
One thing that immediately stands out is the use of push $ 0x2; pop% eax "to move value 2 to EAX when simple" mov $ 0x2,% eax "?
My assumption is that the command encoding for push $ 0x2 ", 6a02 , is easier to change in hexadecimal. Both manually and programmatically. I would suggest that someone tried to use the CPUID 3 function somewhere to get the processor serial number and found that it is not supported, and then switches to using function 2.
" ret $ 0x10 " at the end is also unusual. The RET form IMM16 of the RET command returns to the caller and then removes the IMM16 stack from the line. The fact that the caller is responsible for expressing arguments from the stack after the function returns implies that this does not use the standard x86 calling convention.
In fact, a quick look at C # code shows that it uses CallWindowProc () to call the build function. The documentation for CallWindowProc () shows that the assembler code implements a C function with a signature of the type:
__stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam);
__ stdcall is a special function convention used by the 32-bit Windows APIs.
The assembly code uses 0x10 (% ebp) , which is the third argument to the function, as a character array to store the output from the CPUID command . (After the prologue of the standard function on x86, 8 (% ebp) is the first argument. 0xc (% ebp) is the second 4-byte argument and 0x10 (% ebp) is the third). The third parameter in our prototype window procedure function above: wParam . It is used as the out parameter and is the only parameter used in the assembly code.
The last interesting thing for assembler code is that it compresses the EDI and EBX registers without saving them, violating the __ stdcall calling convention . This error is apparently hidden when the function is called via CallWindowProc () , but it will be detected if you try to write your own main function in C to test the build code ( cpuid-main.c ):
#include <stdio.h>
The build version corrected to save and restore EDI , EBX and use the CPUID function is as follows:
.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
The _cpuid_wind_proc @ 16 character name is how the __ stdcall names are garbled on 32-bit Windows. @ 16 is the number of bytes that take parameters. (Four parameters, each of which takes four bytes on 32-bit Windows, adds up to 16)
Now I'm ready to port the code to x64.
- When consulting this handy ABI table, I can see that the first four parameters are passed to RCX , RDX , R8 and R9 , so wParam is in R8 .
- Intel documentation tells me that the CPUID team clobbers EAX , EBX , ECX and EDX . EBX is the lower half of RBX , which is the saved GPR in the ABI ("saved GPR" here means the general register that its contents should be saved through a function call), so I had to save RBX before executing the CPUID instruction and restore the RBX .
Here is the x64 build:
.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
As you can see, the x64 version is shorter and easier to write. There is only one x64 convention invocation function, so we donβt need to worry about __ stdcall .
Create an x64 build function along with cpuid-main.c and compare its output with this VBScript ( cpuid.vbs ):
Set objProc = GetObject("winmgmts:root\cimv2:Win32_Processor='cpu0'") WScript.echo objProc.ProcessorId
Run cpuid.vbs with
wscript cpuid.vbs
and check for matching outputs. (I actually cross-compiled with MinGW-w64 on Linux and ran the program under Wine64 emulation while running C and building up to this point.)
With the CPUID x64 feature working, I'm now ready to integrate the code back into C #.
- Parse cpuid-x64.exe to get the operation codes and insert them as a new byte array ( code_x64 ).
- Modify ExecuteCode () to determine whether CPUID x86 or x64 should be run by testing for IntPtr.Size == 8 in IsX64Process () .
Finally, modify ProcessorId () to create a hexadecimal string with:
string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8"));
Using "X8" instead of "X" ensures that UInt32 is formatted as a hexadecimal value with zero padding. Otherwise, you cannot determine which digits came from EDX and which ones from EAX when you combine them into one line.
What is it.