My Hakyll Blog - Getting CPUID info in pure Ruby (original) (raw)

Posted on July 15, 2013

Here’s a fun little exercise: write a pure Ruby interface to the x86 cpuid instruction.

In order to use the cpuid instruction from Ruby, we’ll need to craft and execute some machine code. Before we can do that, though, we’ll need to properly allocate the memory for our code.

Executable Memory Allocation

Modern operating systems mark regions of writable memory as non-executable to prevent various exploits. Memory is protected per page, so we’ll need to allocate the memory for our code such that it is:

Below, you’ll see how we can use FFI to wrap posix_memalign (to allocate our memory) and mprotect (to set the protection):

module MemUtil extend FFI::Library ffi_lib "c" attach_function :mprotect, [:pointer, :size_t, :int], :int attach_function :posix_memalign, [:pointer, :size_t, :size_t], :int attach_function :getpagesize, [], :int attach_function :memset, [:pointer, :int, :size_t], :void

PROT_NONE = 0x00 PROT_READ = 0x01 PROT_WRITE = 0x02 PROT_EXEC = 0x04

def self.allocate_pages(count) out_pointer = FFI::MemoryPointer.new(:pointer, 1) posix_memalign(out_pointer, page_size, page_size * count) out_pointer.read_pointer end

def self.allocate_executable(code) pages = (code.size / page_size.to_f).ceil mem = allocate_pages(pages) memset(mem, 0x90, pages * page_size) mem.put_array_of_uint8(0, code) mprotect(mem, code.size, PROT_READ | PROT_EXEC)

mem

end

def self.page_size @page_size ||= getpagesize end end

Given an array of bytes, MemUtil::allocate_executable allocates one or more pages of memory, fills it with NOPs, writes the machine code, and then sets the protection.

CPUID machine code

Here’s the fun part, where we assemble a cdecl function and encode the machine code as an array. I won’t go into too much detail about the assembler 1, but something like this should do:

module CPUID X32_CODE = [ 0x55, # pushl %ebp 0x89, 0xE5, # movl %esp,%ebp 0x57, # pushl %edi 0x56, # pushl %esi 0x8B, 0x45, 0x08, # movl 0x08(%ebp),%eax 0x89, 0xDE, # movl %ebx,%esi 0x0F, 0xA2, # cpuid 0x87, 0xDE, # xchgl %esi,%ebx 0x8B, 0x7D, 0x0C, # movl 0x0c(%ebp),%edi 0x89, 0x07, # movl %eax,(%edi) 0x89, 0x77, 0x04, # movl %esi,0x04(%edi) 0x89, 0x4F, 0x08, # movl %ecx,0x08(%edi) 0x89, 0x57, 0x0C, # movl %edx,0x0c(%edi) 0x5E, # popl %esi 0x5F, # popl %edi 0x5D, # popl %ebp 0xC3, # ret ]

X64_CODE = [ 0x55, # pushq %rbp 0x48, 0x89, 0xE5, # movq %rsp,%rbp 0x49, 0x89, 0xF0, # movq %rsi,%r8 0x89, 0xF8, # movl %edi,%eax 0x89, 0xDE, # movl %ebx,%esi 0x0F, 0xA2, # cpuid 0x87, 0xDE, # xchgl %esi,%ebx 0x41, 0x89, 0x00, # movl %eax,(%r8) 0x41, 0x89, 0x70, 0x04, # movl %esi,0x04(%r8) 0x41, 0x89, 0x48, 0x08, # movl %ecx,0x08(%r8) 0x41, 0x89, 0x50, 0x0C, # movl %edx,0x0c(%r8) 0x5D, # popq %rbp 0xC3, # ret ] end

X32_CODE and X64_CODE contain our machine code for 32bit and 64bit architectures, respectively. The functions take an integer and a pointer to an array of 4 32bit integers; the former is an argument to cpuid (via eax) that specifies what info we want returned, and the later is where we write out the resulting data.

All that’s left now is to set up the trampoline and give it a spin:

module CPUID CPUID_FUNCTION =
FFI::Function.new(:void, [ :uint, :pointer ], MemUtil.allocate_executable(FFI::Pointer.size == 4 ? X32_CODE : X64_CODE))

def self.run_cpuid(fn) buffer = FFI::MemoryPointer.new(:uint32, 4) CPUID_FUNCTION.call(fn, buffer) buffer.get_array_of_uint32(0, 4) end end

vendor_string = CPUID.run_cpuid(0).inject("") do |str, reg| 0.upto(3) do |idx| str << ((reg >> (idx * 8)) & 0xFF).chr end str end

puts vendor_string

On my MacBook, this prints:

GenuntelineI

The Full Listing

require 'ffi'

module CPUID X32_CODE = [ 0x55, # pushl %ebp 0x89, 0xE5, # movl %esp,%ebp 0x57, # pushl %edi 0x56, # pushl %esi 0x8B, 0x45, 0x08, # movl 0x08(%ebp),%eax 0x89, 0xDE, # movl %ebx,%esi 0x0F, 0xA2, # cpuid 0x87, 0xDE, # xchgl %esi,%ebx 0x8B, 0x7D, 0x0C, # movl 0x0c(%ebp),%edi 0x89, 0x07, # movl %eax,(%edi) 0x89, 0x77, 0x04, # movl %esi,0x04(%edi) 0x89, 0x4F, 0x08, # movl %ecx,0x08(%edi) 0x89, 0x57, 0x0C, # movl %edx,0x0c(%edi) 0x5E, # popl %esi 0x5F, # popl %edi 0x5D, # popl %ebp 0xC3, # ret ]

X64_CODE = [ 0x55, # pushq %rbp 0x48, 0x89, 0xE5, # movq %rsp,%rbp 0x49, 0x89, 0xF0, # movq %rsi,%r8 0x89, 0xF8, # movl %edi,%eax 0x89, 0xDE, # movl %ebx,%esi 0x0F, 0xA2, # cpuid 0x87, 0xDE, # xchgl %esi,%ebx 0x41, 0x89, 0x00, # movl %eax,(%r8) 0x41, 0x89, 0x70, 0x04, # movl %esi,0x04(%r8) 0x41, 0x89, 0x48, 0x08, # movl %ecx,0x08(%r8) 0x41, 0x89, 0x50, 0x0C, # movl %edx,0x0c(%r8) 0x5D, # popq %rbp 0xC3, # ret ]

def self.run_cpuid(fn) buffer = FFI::MemoryPointer.new(:uint32, 4) CPUID_FUNCTION.call(fn, buffer) buffer.get_array_of_uint32(0, 4) end

module MemUtil extend FFI::Library ffi_lib "c" attach_function :mprotect, [:pointer, :size_t, :int], :int attach_function :posix_memalign, [:pointer, :size_t, :size_t], :int attach_function :getpagesize, [], :int attach_function :memset, [:pointer, :int, :size_t], :void

PROT_NONE  = 0x00
PROT_READ  = 0x01
PROT_WRITE = 0x02
PROT_EXEC  = 0x04

def self.allocate_pages(count)
  out_pointer = FFI::MemoryPointer.new(:pointer, 1)
  posix_memalign(out_pointer, page_size, page_size * count)
  out_pointer.read_pointer
end

def self.allocate_executable(code)
  pages = (code.size / page_size.to_f).ceil
  mem   = allocate_pages(pages)
  memset(mem, 0x90, pages * page_size)
  mem.put_array_of_uint8(0, code)
  mprotect(mem, code.size, PROT_READ | PROT_EXEC)

  mem
end

def self.page_size
  @page_size ||= getpagesize
end

end

CPUID_FUNCTION =
FFI::Function.new(:void, [ :uint, :pointer ], MemUtil.allocate_executable(FFI::Pointer.size == 4 ? X32_CODE : X64_CODE)) end


  1. If you’re new to assembly programming, for a first book I highly recommend “Assembly Language Step-by-Step: Programming with Linux” by Jeff Duntemann.