内联汇编代码以获取CPU ID

Tom*_*zzo 3 .net c# 64-bit assembly winapi

我在这里找到了一段很好的代码,它使用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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
}
Run Code Online (Sandbox Code Playgroud)

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

sco*_*ttt 22

这是您修改的代码,以便在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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

反汇编cpuid-x86.bin.我使用了udis86udcli.

$ 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 
Run Code Online (Sandbox Code Playgroud)

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

我的猜测是" push $ 0x2 ",6a02的指令编码更容易以十六进制形式修改.手动和编程.我想有人试图使用CPUID函数3获取处理器序列号,发现它不支持然后切换到使用功能2.

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

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

__stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam);
Run Code Online (Sandbox Code Playgroud)

__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 <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;
}
Run Code Online (Sandbox Code Playgroud)

修复了用于保存和恢复EDI,EBX和使用CPUID功能1 的程序集的版本是这样的:

    .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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 通过查阅这个方便的ABI表,我看到前四个参数在RCX,RDX,R8R9中传递,因此wParamR8中.
  • 英特尔文档告诉我CPUID指令破坏了EAX,EBX,ECXEDX.EBX是下半部RBX这是一个保存GPR在ABI("保存GPR"在这里是指应在整个函数调用保持其内容的通用寄存器),所以我确信保存RBX执行之前CPUID指令和恢复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
Run Code Online (Sandbox Code Playgroud)

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

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

Set objProc = GetObject("winmgmts:root\cimv2:Win32_Processor='cpu0'")
WScript.echo objProc.ProcessorId
Run Code Online (Sandbox Code Playgroud)

运行cpuid.vbs

wscript cpuid.vbs
Run Code Online (Sandbox Code Playgroud)

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

随着x64程序集CPUID功能正常工作,我现在准备将代码集成回C#.

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

最后,更改ProcessorId()以生成十六进制字符串:

string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8"));
Run Code Online (Sandbox Code Playgroud)

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

就是这样.