exceptionassemblyx86-64cpu

How to catch EXCEPTION_PRIV_INSTRUCTION from RDPMC directly in Assembly (and without SEH)?


I'm experimenting with measuring CPU's instructions latency and throughput on P and E cores using RDPMC on Win 11, something like that:

    MOV ECX, 0x40000000 ; Instructions Counter
    RDPMC ; Read Performance-Monitoring Counter
    SHL RDX, 32
    OR RAX, RDX ; Value in RAX
    ; ... instruction to be tested and so on

Everything works like a charm except for one minor but annoying issue.

If I forget to set bit 8 of the CR4 register (which I currently configure via my own Windows driver), I get an EXCEPTION_PRIV_INSTRUCTION (0xC0000096) at RDPMC, as expected in a user-mode application.

Question: Is there any elegant way to catch and handle this exception directly in assembly code?

Ironically, I can't read and check the CR4 register before executing RDPMC because reading CR4 requires privileged execution at Ring 0 as well.

But for example, this code works fine:

#include <intrin.h>
#include <stdio.h>
#include <windows.h>

int main() {
    unsigned int ecx = 0x40000000; // Counter index
    __try {
        unsigned __int64 value = __readpmc(ecx); // RDPMC
        printf("PMC[0x%X] value: %llu\n", ecx, value);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        printf("Exception occurred while reading RDPMC\n");
    }
    return 0;
}

However, I would like to achieve the same "try/except" behavior directly in assembly in simple way and without involving SEH, ideally in an OS-independent manner. Is that possible, or is it fundamentally unavoidable?


Solution

  • Well, now I've got this (thanks to Nate Eldredge). If CPU exceptions are first handled by the kernel, then they should be propagated to user space in an OS-specific way. I had hoped it could be as simple as I did in the past with the PDP-11 and RT-11, about 35+ years ago, using a simple trap. But, well, the world nowadays is slightly more complicated.

    The OS I'm actually targeting is Windows x64, so this answer is specific to that, using SEH.

    After digging through old forums and documentation, I was able to get things done using FASM.

    Here is a minimal working example with an SEH handler:

    format PE64 CONSOLE 
    entry start
    
    include 'seh64.inc'
    
    start:
        SUB RSP,8*(4+1)
        LEA RCX,[hello]
        call [printf]
    
    .try handler
        MOV ECX,0x40000000 ; Instructions Counter
        RDPMC ; Read Performance-Monitoring Counter
    .end
    
    safe_place:
        LEA RCX, [finish]
        call [printf]
        XOR ECX,ECX
        call [ExitProcess]
    
    handler:
        SUB RSP,8*(4+1)
        MOV qword [R8+CONTEXT64.Rip],safe_place
        LEA RCX,[exception]
        call [printf]
        XOR EAX,EAX
        ADD RSP,8*(4+1)
        retn
    
    section '.data' data readable writeable
        hello db "Hello, SEH!",10,0
        finish db "Sucessfully finished",10,0
        exception db "Instruction caused exception",10,0
    
    data seh
    end data
    
    section '.idata' import data readable
        library kernel32,'KERNEL32.DLL', msvcrt, 'msvcrt.dll'
        import kernel32, ExitProcess,'ExitProcess'
        import msvcrt, printf, 'printf'
    

    Now exception on RDPMC will not cause application crash, and will be landed into my hands in handler:

    >sehtest.exe
    Hello, SEH!
    Instruction caused exception
    Sucessfully finished
    

    Of course, this will work for any kind of exception as well, for example:

    .try handler
        XOR EAX,EAX
        MOV dword [EAX], 0
    .end
    

    or

    .try handler
        XOR  EBX,EBX 
        DIV  EBX
    .end
    

    Content of seh64.inc:

    include 'INCLUDE\win64a.inc'
    
    macro enqueue list,item
     { match any,list \{ list equ list,item \}
       match ,list \{ list equ item \} }
    
    macro dequeue list,item
     { done@dequeue equ
       match first=,rest,list
       \{ item equ first
          list equ rest
          restore done@dequeue \}
       match :m,done@dequeue:list
       \{ item equ m
          list equ
          restore done@dequeue \}
       match ,done@dequeue
       \{ item equ
          restore done@dequeue
       \} }
    
    macro queue list,index,item
     { local copy
       copy equ list
       rept index+1 \{ dequeue copy,item \} }
    
    macro data directory
     { done@data equ
       match =3,directory
       \{ local l_infos,_info,_end
          l_infos equ
          align 4
          match list,l_handlers
          \\{
             irp _handler,list
             \\\{ local rva$
                rva$ = rva $
                enqueue l_infos,rva$
                db 19h,0,0,0
                dd _handler,0
             \\\}
          \\}
          data 3
          match list,l_begins
          \\{
             irp _begin,list
             \\\{
                dequeue l_ends,_end
                dequeue l_infos,_info
                dd _begin
                dd _end
                dd _info
             \\\}
          \\}
          restore done@data
       \}
       match ,done@data
       \{ data directory
          restore done@data \} }
    
    l_begins equ
    l_ends equ
    l_handlers equ
    
    macro .try handler
     { local ..try
       __TRY equ ..try
       local ..end
       __END equ ..end
       local ..catch
       __CATCH equ ..catch
       __TRY:
       if ~ handler eq
        virtual at handler
        __CATCH:
        end virtual
       end if }
    
    macro .catch
     { jmp __END
       __CATCH: }
    
    macro .end
     { __END:
       enqueue l_begins,rva __TRY
       enqueue l_ends,rva __END
       enqueue l_handlers,rva __CATCH
       restore __TRY
       restore __END
       restore __CATCH }
    
    seh equ 3
    CONTEXT64.Rip = 0F8h
    

    Be very careful with this code above, it is just "quick and dirty" (but at least functional) example.

    Update

    And as suggested by Margaret Bloom, here is a code snippet that uses AddVectoredExceptionHandler. This time EuroAssembler:

    EUROASM AutoSegment=ON, CPU=X64, SIMD=AVX2
    %^SourceName PROGRAM Format=PE, Width=64, Model=Flat, IconFile=, Entry=main
    INCLUDE winscon.htm, winabi.htm, cpuext64.htm
    
    helloMsg D "Hello, Exception!",0
    finishMsg D "Sucessfully finished",0
    exceptionMsg D "Instruction caused exception",0
    Handle DQ 0
    
    main: nop
        StdOutput helloMsg, Eol=Yes, Console=Yes
    
        WinABI AddVectoredExceptionHandler, 1, VectoredHandler
        MOV [Handle],RAX
        
        MOV ECX, 0x40000000 ; Counter 
        RDPMC ; 2 bytes (0F33) EXCEPTION_PRIV_INSTRUCTION
    
        WinABI RemoveVectoredExceptionHandler, [Handle]    
        StdOutput finishMsg, Eol=Yes, Console=Yes
        TerminateProgram
    
    VectoredHandler PROC
        PUSH RBX
        PUSH RSI
        PUSH RDI
    
        MOV RBX, RCX        ; EXCEPTION_POINTERS
        MOV RDI, [RBX + 8]  ; CONTEXT
        MOV RSI, [RBX]      ; EXCEPTION_RECORD
        MOV EAX, [RSI]      ; ExceptionCode
        CMP EAX, 0C0000096h ; EXCEPTION_PRIV_INSTRUCTION
        JNE continue_search
        ADD [RDI + 0F8h], 2, DATA=Q ; Rip += 2 (adjust for RDPMC)
        StdOutput exceptionMsg, Eol=Yes, Console=Yes
        MOV EAX, 0FFFFFFFFh ; EXCEPTION_CONTINUE_EXECUTION   
        JMP return
    continue_search:
        MOV EAX,1           ; EXCEPTION_CONTINUE_SEARCH
    return:
        POP RDI
        POP RSI
        POP RBX
        RET
    ENDP
    
    ENDPROGRAM
    

    and it works as well:

    >vecdemo.exe
    Hello, Exception!
    Instruction caused exception
    Sucessfully finished