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?
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