如何从C#调用CPU指令?

11
我的处理器(Intel i7)支持“POPCNT指令”,我想从我的C#应用程序中调用它。这是可能的吗?
我相信我在某个地方读到过这不可能,但如果JIT发现它可用,它将调用它,但我需要调用哪个函数来替换此指令?
Popcount 在循环中被调用数百万次,因此如果可能的话,我希望能够进行CPU优化。

4
C# 是适合这个任务的语言吗?我认为我们使用像 C# 这样的语言是因为我们不必(那么努力地)考虑 CPU 指令。 - Doug Dawson
1
这个问题已经在StackOverFlow上得到了提问和回答。 - Kyle Williamson
@KyleWilliamson 那个问题是关于如何确定CPU是否支持该指令,而不是如何调用它。 - crashmstr
6
如何用螺丝刀钉这个钉子?我知道这不是正确的工具,但我讨厌锤子却喜欢螺丝刀。如果你必须这样做,那么你需要使用另一种语言。如果这对你来说不明显,那么恐怕你会搞砸实施。 - Ed S.
1
这个问题还有一些相关信息,请参考这个回答。另一种方法是使用非托管C++编写瓶颈部分,具体请参考这里 - phuclv
显示剩余3条评论
1个回答

15

你想玩火,而我们喜欢玩火...

(这句话可能是引用或暗示某种技术或行为,需要更多上下文来确定确切的翻译。)
class Program
{
    const uint PAGE_EXECUTE_READWRITE = 0x40;
    const uint MEM_COMMIT = 0x1000;

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, uint flProtect);

    private delegate int IntReturner();

    static void Main(string[] args)
    {
        List<byte> bodyBuilder = new List<byte>();
        bodyBuilder.Add(0xb8); // MOV EAX,
        bodyBuilder.AddRange(BitConverter.GetBytes(42)); // 42
        bodyBuilder.Add(0xc3);  // RET
        byte[] body = bodyBuilder.ToArray();
        IntPtr buf = VirtualAlloc(IntPtr.Zero, (IntPtr)body.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        Marshal.Copy(body, 0, buf, body.Length);

        IntReturner ptr = (IntReturner)Marshal.GetDelegateForFunctionPointer(buf, typeof(IntReturner));
        Console.WriteLine(ptr());
    }
}

(这个汇编的小例子将简单地返回42……我认为这是这个答案的完美数字:-))

最终的诀窍在于:

A)您必须知道与您要编写的asm相对应的操作码

B)您使用VirtualAlloc使一个内存页面可执行

C)以某种方式将您的操作码复制到那里

(代码来自http://www.cnblogs.com/netact/archive/2013/01/10/2855448.html

好的……另一个就像网站上写的一样(减去了uint -> IntPtr dwSize上的错误),这个就是应该写的方式(或者至少比原来多1个……我会将所有内容封装在一个IDisposable类中,而不是使用try... finally

class Program
{
    const uint PAGE_READWRITE = 0x04;
    const uint PAGE_EXECUTE = 0x10;
    const uint MEM_COMMIT = 0x1000;
    const uint MEM_RELEASE = 0x8000;

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, uint flProtect);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool VirtualProtect(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, out uint lpflOldProtect);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool VirtualFree(IntPtr lpAddress, IntPtr dwSize, uint dwFreeType);

    private delegate int IntReturner();

    static void Main(string[] args)
    {
        List<byte> bodyBuilder = new List<byte>();
        bodyBuilder.Add(0xb8); // MOV EAX,
        bodyBuilder.AddRange(BitConverter.GetBytes(42)); // 42
        bodyBuilder.Add(0xc3);  // RET

        byte[] body = bodyBuilder.ToArray();

        IntPtr buf = IntPtr.Zero;

        try
        {
            // We VirtualAlloc body.Length bytes, with R/W access
            // Note that from what I've read, MEM_RESERVE is useless
            // if the first parameter is IntPtr.Zero
            buf = VirtualAlloc(IntPtr.Zero, (IntPtr)body.Length, MEM_COMMIT, PAGE_READWRITE);

            if (buf == IntPtr.Zero)
            {
                throw new Win32Exception();
            }

            // Copy our instructions in the buf
            Marshal.Copy(body, 0, buf, body.Length);

            // Change the access of the allocated memory from R/W to Execute
            uint oldProtection;
            bool result = VirtualProtect(buf, (IntPtr)body.Length, PAGE_EXECUTE, out oldProtection);

            if (!result)
            {
                throw new Win32Exception();
            }

            // Create a delegate to the "function"
            // Sadly we can't use Funct<int>
            var fun = (IntReturner)Marshal.GetDelegateForFunctionPointer(buf, typeof(IntReturner));

            Console.WriteLine(fun());
        }
        finally
        {
            if (buf != IntPtr.Zero)
            {
                // Free the allocated memory
                bool result = VirtualFree(buf, IntPtr.Zero, MEM_RELEASE);

                if (!result)
                {
                    throw new Win32Exception();
                }
            }
        }
    }
}

3
最好在复制后调用VirtualProtect,添加X位并移除W。因为实施W^X似乎有助于提高安全性。 - Ben Voigt
顺便说一下,popcnt eax, [esp + 4] 对应的机器码是 F3 0F B8 44 24 04。对于 win64 调用规约,可以使用 F3 0F B8 C1 作为 popcnt eax, ecx 的机器码。 - harold
@BenVoigt 现在我已经使用了一些 try... finallyVirtualProtect,感觉更加...清爽 :) - xanatos
@xanatos:是的,但如果有人决定让GetDelegateForFunctionPointer喜欢它们,那么从技术上讲,为什么这不可能呢?看起来是非常有用的功能。 - Erti-Chris Eelmaa
@ChrisEelmaa 我不知道为什么微软决定使用GetDelegateForFunctionPointer来支持通用委托会变得过于复杂。 - xanatos
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接