【问题标题】:Inline Assembly Code to Get CPU ID内联汇编代码以获取 CPU ID
【发布时间】:2013-05-09 11:17:56
【问题描述】:

我发现了一段不错的代码here,它使用 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) 这段代码 sn-p 对任何类型的处理器都有效吗?我在使用 Intel 内核的笔记本电脑上对其进行了测试,它可以正常工作……但 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”。代码 sn-p 的结果是“F0B2FF0CA0000”。为什么?哪一个是正确的?

【问题讨论】:

  • 你真正想要达到什么目的?如果要获得“唯一处理器 ID”,为什么不在第二个 sn-p 中使用 C#/托管方法,让自己的生活更轻松?
  • 因为两个原因:第一个是托管代码太慢了,第二个是我不确定哪个返回正确的信息。
  • 当您说它很慢时,您是否对这两种方法进行了计时?我怀疑其中会有很多内容,并且无论如何您都将 ASM 代码包装在托管代码中,因此任何“收益”都会很快再次“丢失”?你的问题是完全正确的,我不会敲它,只是有时值得使用管理方法,可以在任何处理器上完成工作,而且你不会面临任何令人讨厌的惊喜!假设有人在 ARM 处理器上使用它,因为您的库被移植到平板电脑上……然后呢?!
  • 这就是我想知道的……这对每种 CPU 都有效吗?我不知道,但无论如何,该应用程序永远不会移植到任何移动设备上。无论如何......我现在要做一个小基准,看看发生了什么......同时我仍然想知道为什么我使用这两种方法得到完全不同的结果......
  • 我希望您查询处理器 ID 一次,因为它不会改变。我怀疑在单次调用期间任何性能差异都会显着。

标签: c# .net winapi assembly 64-bit


【解决方案1】:

您的代码修改为在 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 序列号

  • 您使用了 CPUID 函数 2(通过在执行 CPUID 指令之前将 2 置于 EAX 中)。如果您阅读 IntelAMD CPUID 应用说明,您会发现这会读回缓存和 TLB 硬件配置,并且仅在 Intel 上受支持。
  • 我修改了您的代码以使用 CPUID 函数 1,它读回 CPU 的步进、型号和系列。这符合WIN32_Processor.ProcessorID 的行为
  • 现代 x86 CPU 的序列号在“下线”的相同单元中没有唯一的序列号。处理器序列号只能通过 CPUID 函数 3 在 Pentium 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))

反汇编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 

立即突出的一件事是,当一个简单的“mov $0x2, %eax" 可以吗?

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

结尾的“ret $0x10”也很不寻常。 RET 指令的 RET IMM16 形式返回给调用者,然后弹出 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。它用作输出参数,是汇编代码中唯一使用的参数。

关于汇编代码的最后一个有趣的事情是它破坏了寄存器 EDIEBX 而不保存它们,这违反了 __stdcall 调用约定.当通过 CallWindowProc() 调用函数时,此错误显然是潜在的,但如果您尝试用 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;
}

固定保存和恢复EDIEBX并使用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

符号名称 _cpuid_wind_proc@16__stdcall 函数名称在 32 位 Windows 上的错位方式。 @16 是参数占用的字节数。 (4个参数在32位Windows上各占4个字节加起来16个)

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

  • 通过咨询this handy ABI table看到前四个参数传入RCXRDXR8R9 所以 wParamR8 中。
  • 英特尔文档告诉我,CPUID 指令会破坏 EAXEBXECX >EDXEBXRBX 的下半部分,是 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

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

随着 x64 程序集 CPUID 函数的工作,我现在准备将代码集成回 C#。

  • 反汇编 cpuid-x64.exe 以获取操作码并将其粘贴为新的字节数组 (code_x64)。
  • 更改ExecuteCode(),通过测试IsX64Process( )

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

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

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

就是这样。

【讨论】:

    【解决方案2】:

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

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

    另请注意,此功能在 AMD 处理器上不可用,但代码仍应无错误地执行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-16
      • 2015-03-17
      • 2019-11-30
      • 1970-01-01
      • 2012-05-01
      相关资源
      最近更新 更多