Here's your code 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;
/* 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;
}
}
}
I made trivial modifications to the C# part without compiling the code (I don't have a Windows dev machine setup at the moment) so if there are syntax errors please just make the obvious fix.
I want to stress one very important point: what your original code was reading back was NOT a CPU serial number:
- You used CPUID function 2 (by placing 2 in EAX before executing the CPUID instruction). If you read the Intel and AMD CPUID application notes you'll see that this reads back the cache and TLB hardware configuration and is only supported on Intel.
- I modified your code to use CPUID function 1, which reads back the stepping, model, and family of a CPU. This matches the behavior of WIN32_Processor.ProcessorID
- Modern x86 CPUs don't have a serial number that is unique among otherwise identical units "rolling off the assembly line". Processor serial numbers were only available on Pentium 3's through CPUID function 3.
I'll now explain the process and tools I used.
Paste the 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))
Disassemble 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 why use "push $0x2; pop %eax" to move the value 2 into EAX when a simple "mov $0x2, %eax" will do?
My guess is that the instruction encoding for "push $0x2", 6a02, is easier to modify in hexadecimal form. Both by hand and programmatically. I'd guess somebody somewhere tried to use CPUID function 3 to get the processor serial number and found that it wasn't supported then switched to using function 2.
The "ret $0x10" at the end is also unusual. The RET IMM16 form of the RET instruction returns to the caller then pops IMM16 bytes off the stack. The fact that the callee is responsible for popping arguments off the stack after function return implies that this is not using the standard x86 calling convention.
Indeed, a quick peek into the C# code reveals that it's using CallWindowProc() to invoke the assembly function. The documentation for CallWindowProc() shows that the assembly code is implementing a C function with a signature like:
__stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam);
__stdcall is the special function calling convention used by 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 instruction. (After a standard function prologue 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 window procedure function prototype above is wParam. It's used as an out parameter and is the only parameter used in the assembly code.
The last interesting thing about the assembly code is that it clobbers the registers EDI and EBX without saving them, violating the __stdcall calling convention. This bug is apparently latent when calling the function through CallWindowProc() but will reveal itself if you try to write your own main function in C to test the assembly code (cpuid-main.c):
#include <stdio.h>
#include <stdint.h>
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;
}
A version of the assembly fixed to save and restore EDI, EBX and use CPUID function 1 is like this:
.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 symbol name _cpuid_wind_proc@16 is how __stdcall function names are mangled on 32 bit Windows. The @16 is the number of bytes the parameters take up. (Four parameters each taking four bytes on 32 bit Windows adds up to 16)
Now I'm ready to port the code to x64.
- By consulting this handy ABI table I see that the first four parameters are passed in RCX, RDX, R8, and R9 so wParam is in R8.
- The Intel documentation tells me that the CPUID instruction clobbers EAX, EBX, ECX, and EDX. EBX is the lower half of RBX which is a saved GPR in the ABI ("saved GPR" here means a general purpose register that should retain its contents across a function call) so I made sure to save RBX before executing the CPUID instruction and restore RBX afterwards.
Here's the x64 assembly:
.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's only one function calling convention on x64 so we don't have to worry about __stdcall.
Build the x64 assembly 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 verify the outputs match. (I actually cross compiled with MinGW-w64 on Linux and ran the program under Wine64 emulation while doing the C and assembly work up till this point.)
With the x64 assembly CPUID function working, I'm now ready to integrate the code back into C#.
- Disassemble cpuid-x64.exe to get the opcodes and paste them as a new byte array (code_x64).
- Change ExecuteCode() to determine whether to run the x86 or x64 version of the CPUID code by testing for IntPtr.Size == 8 in IsX64Process().
Finally, change ProcessorId() to produce the hexadecimal string with:
string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8"));
Using "X8" instead of just "X" ensures that the UInt32 is formatted as an 8 digit hexadecimal value with zero padding. Otherwise, you can't tell which digits came from EDX and which from EAX when you concatenate them into a single string.
And that's it.