Jump to content


Photo

DebugBIF - debug built-in functions (was DllCallDebugger)


  • Please log in to reply
14 replies to this topic

#1 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 13 October 2008 - 12:13 PM

DebugBIF( [ func_to_debug, func_to_call, event_info ] ) for AutoHotkey v1.0.48.01-04
Provides an effective way to debug a script's use of built-in functions.
; Requires AutoHotkey v1.0.48 and the appropriate version of LowLevel,
;  which can be found here:  http://www.autohotkey.com/forum/topic26300.html
 
/*
    Function: DebugBIF
 
        Registers a user-defined function to be called when a built-in function either
        sets ErrorLevel to a non-zero value or returns an empty string, as appropriate.
 
    Syntax:
 
        DebugBIF( [ func_to_debug, func_to_call, event_info ] )
 
    Parameters:
 
        func_to_debug   - The name of the built-in function to debug.
                          If omitted or empty, all applicable functions are affected.
 
        func_to_call    - The name of a user-defined function to call.
                          If omitted or empty, DebugBIF_DefaultHandler is used.
 
        event_info      - An integer to pass via A_EventInfo.
                          If omitted or empty, A_EventInfo contains a boolean value
                          indicating whether ErrorLevel triggered the current call.
*/
 
DebugBIF(func_to_debug="", func_to_call="", event_info="")
{
    static DebugErrorLevel, DebugResult, DebugInitd
 
    if func_to_call =
        func_to_call = DebugBIF_DefaultHandler
 
    if func_to_debug =
    {
        if DebugInitd
            return
        func_to_debug = RegExMatch,RegExReplace,NumGet,NumPut,GetKeyState
                    ,Mod,ASin,ACos,Sqrt,Log,Ln,RegisterCallback,DllCall
        Loop, Parse, func_to_debug, `,
            DebugBIF(A_LoopField, func_to_call)
        DebugInitd := true
        return
    }
    ListLines Off
 
    if func_to_debug in DllCall,RegExMatch,RegExReplace
        use_error_level := true
    else
        use_error_level := false
 
    if event_info =
        event_info := use_error_level
 
    LowLevel_init()
    if !(bif := __findFunc(func_to_debug)) || !NumGet(bif+49,0,"char")
    || !(callback := RegisterCallback(func_to_call,"C F",4,event_info))
        {
        ListLines On
        return
        }
 
    bif_native := NumGet(bif+4)
 
    if !VarSetCapacity(DebugErrorLevel)
    {
        VarSetCapacity(DebugErrorLevel, 56), NumPut(0xC35D5E5F, NumPut(0x10C4830C, NumPut(0x55FF5756, NumPut(0x1C75FF20, NumPut(0x75FF0E74, NumPut(0x3038800C, NumPut(0xC4830840, NumPut(0x8B08458B, NumPut(0x1055FF56, NumPut(0x1C75FF3E, NumPut(0x8B2075FF, NumPut(0x5718758B, NumPut(0x56EC8B55, NumPut(&DebugErrorLevel+4, DebugErrorLevel))))))))))))))
        VarSetCapacity(DebugResult, 60), NumPut(0x0000C35D, NumPut(0x5E5F10C4, NumPut(0x830855FF, NumPut(0x57561875, NumPut(0xFF1C75FF, NumPut(0x0E750038, NumPut(0x80068B15, NumPut(0x7500087E, NumPut(0x830CC483, NumPut(0x0C55FF56, NumPut(0x1875FF3E, NumPut(0x8B1C75FF, NumPut(0x5714758B, NumPut(0x56EC8B55, NumPut(&DebugResult+4, DebugResult)))))))))))))))
    }
 
    if use_error_level
        __mcode(func_to_debug, ""
            . "68" __mcodeptr(bif_native)
            . "68" __mcodeptr(callback)
            . "68" __mcodeptr(__getVar(ErrorLevel))
            . "FF15" __mcodeptr(&DebugErrorLevel)
            . "83C40C"
            . "C3")
    else
        __mcode(func_to_debug, ""
            . "68" __mcodeptr(bif_native)
            . "68" __mcodeptr(callback)
            . "FF15" __mcodeptr(&DebugResult)
            . "83C408"
            . "C3")
    
    ListLines On
}
 
DebugBIF_DefaultHandler(aName, aResultToken, aParam, aParamCount)
{
    static IgnoreRegisterCallback_DllCall:=2
    ListLines Off
    RealErrorLevel := ErrorLevel
    ; If the first two errors are RegisterCallback("DllCall"), assume it is due to the
    ; usual workings of LowLevel.ahk, and ignore them.
    if IgnoreRegisterCallback_DllCall
        if __str(aName)="RegisterCallback" && __getTokenValue(NumGet(aParam+0))="DllCall"
        {
            ListLines On
            return "", --IgnoreRegisterCallback_DllCall
        }
        else
            IgnoreRegisterCallback_DllCall := 0
 
    ListLines
    ;FileAppend, BEGIN DEBUG CODE ~~~~~~~~~~~~~~~~~~~~, NUL
 
    s := __str(aName)
    Loop % aParamCount
    {
        t := NumGet(aParam+(A_Index-1)*4)
        sym := NumGet(t+8)
        s .= "`n   [" A_Index "]: "
        if sym = 3 ; SYM_VAR
            s .= __str(NumGet(NumGet(t+0)+24)) " := "
        v := __getTokenValue(t+0)
        if (sym=0 || sym=3 || (sym=4 && !NumGet(t+4)))
        {
            StringReplace, v, v, ", "", All
            v = "%v%"
        }
        s .= v
    }
    v := __getTokenValue(aResultToken+0)
    if v !=
    {
        sym := NumGet(aResultToken+8)
        if (sym=0 || (sym=4 && !NumGet(aResultToken+4)))
        {
            StringReplace, v, v, ", "", All
            v = "%v%"
        }
        s .= "`n   ret: " v
    }
    else if !A_EventInfo
        s .= "`n`nReturned an empty string."
    if RealErrorLevel && A_EventInfo
        s .= "`n`nErrorLevel = " RealErrorLevel
    MsgBox, 16, %A_ScriptName% - Built-in Function Error, %s%
 
    ErrorLevel := RealErrorLevel
    ;FileAppend, ~~~~~~~~~~~~~~~~~~~~ END DEBUG CODE, NUL
    ListLines On
}
Example:
; Simplest usage:
DebugBIF()

; Examples that should trigger a debug message:
RegExMatch("abc", "(") ; Regex compile error.
NumPut(0, "") ; Invalid second parameter.
NumGet(var, 1) ; Invalid parameters.
Mod(1,0) ; Divide-by-zero.
The script contains code compiled from the following C++ source (plus some AutoHotkey type definitions):
typedef void (* DebugCallbackType)(char *aFuncName, ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount);

MCODE_API void DebugBif1( Var *aErrorLevel					// Pointer to ErrorLevel Var.
						, DebugCallbackType aOnErrorLevel	// Callback to call if ErrorLevel != 0.
						, BuiltInFunctionType aBIF			// BIF to call
						, void *aReturnAddress				// Addressed pushed by expression evaluation, left on stack to reduce code size.
						, ExprTokenType &aResultToken		// Token used to pass func name/receive result.
						, ExprTokenType *aParam[]			// Parameters of this function call.
						, int aParamCount )					// Number of parameters.
{
	// On entry, aResultToken.marker holds the name of the built-in function.
	// This value is overwritten within each built-in function, so save it now.
	char *bif_name = aResultToken.marker;

	// Call the built-in function.
	aBIF(aResultToken, aParam, aParamCount);

	// RegExMatch, RegExReplace and DllCall use non-zero ErrorLevel to indicate an error.
	if (*aErrorLevel->mContents != '0')
		aOnErrorLevel(bif_name, aResultToken, aParam, aParamCount);
}

MCODE_API void DebugBif2( DebugCallbackType aOnBlankResult	// Callback to call if result is "".
						, BuiltInFunctionType aBIF
						, void *aReturnAddress
						, ExprTokenType &aResultToken
						, ExprTokenType *aParam[]
						, int aParamCount )
{
	char *bif_name = aResultToken.marker;

	aBIF(aResultToken, aParam, aParamCount);

	// Several functions use a blank result to indicate an error.
	if (aResultToken.symbol == SYM_STRING && !*aResultToken.marker)
		aOnBlankResult(bif_name, aResultToken, aParam, aParamCount);
}



DllCallDebugger( ErrorCallbackName ) for AutoHotkey v1.0.47.06
DllCallDebugger hooks DllCall with machine code in order to detect non-zero ErrorLevels. Since it is implemented in native code which only calls back to script if an error occurs, it should have little effect on performance.

; Requires AutoHotkey v1.0.47.06 and the appropriate version of LowLevel,
;  which can be found here:  http://www.autohotkey.com/forum/topic26300.html

DllCallDebugger(ErrorCallbackName="DllCallDebugger_DefaultHandler")
{
    static ErrorCallback
    if ErrorCallback
        return
    if !(ErrorCallback := RegisterCallback(ErrorCallbackName,"C F",3))
        return
    
    LowLevel_init()
    DllCallFunc := __findFunc("DllCall")
    DllCallBif := NumGet(DllCallFunc+4)
    ErrorLevelVar := __getVar(ErrorLevel)
    
    pbin:=__mcode("DllCall"
        ; void DllCall(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
        ; {
        ;   DllCallBif(aResultToken, aParam, aParamCount);
        , "FF74240C"                                        ; push [esp+12]
        . "FF74240C"                                        ; push [esp+12]
        . "FF74240C"                                        ; push [esp+12]
        . "B9" DllCallDebugger_FormatPtr(DllCallBif)        ; mov ecx, DllCallBif
        . "FFD1"                                            ; call ecx
        ;   if (*ErrorLevelVar->mContents != '0')
        . "B9" DllCallDebugger_FormatPtr(ErrorLevelVar)     ; mov ecx, ErrorLevelVar
        . "8B09"                                            ; mov ecx, [ecx]
        . "0FBE09"                                          ; movsx ecx, byte ptr [ecx]
        . "83F930"                                          ; cmp ecx, '0'
        . "7407"                                            ; je +7
        ;       ErrorCallback();
        . "B9" DllCallDebugger_FormatPtr(ErrorCallback)     ; mov ecx, ErrorCallback
        . "FFD1"                                            ; call ecx
        ; }
        . "83C40C"                                          ; add esp, 12
        . "C3")                                             ; ret
    
    pbin2:=__mcode("DllCallDebugger_TokenValue"
    /*
    ExprTokenType *token = (ExprTokenType*) aParam[0]->value_int64;

    if (token->symbol == SYM_VAR)
    {
        aResultToken.symbol = SYM_OPERAND;
        aResultToken.marker = token->var->mContents;
    }
    else
    {
        aResultToken.symbol = token->symbol;
        aResultToken.value_int64 = token->value_int64;
    }
    */  , "8B4424088B088B018B50088B4C240483FA03750B428951088B108B028901C38951088B1089118B4004894104C3")

    ; WARNING: This will affect the entire page(s) containing our generated code.
    ; Make our code executable (for systems with Data Execution Prevention enabled):
    DllCall("VirtualProtect","uint",pbin,"uint",22,"uint",0x40,"uint*",0)
    DllCall("VirtualProtect","uint",pbin2,"uint",22,"uint",0x40,"uint*",0)
}

DllCallDebugger_FormatPtr(ptr) {
    FI := A_FormatInteger
    SetFormat, Integer, H
    ptr := RegExReplace(SubStr("00000000" SubStr(ptr+0, 3), -7), "(..)(..)(..)(..)", "$4$3$2$1")
    SetFormat, Integer, %FI%
    return ptr
}

; token MUST be the immediate integer result of a math expression or built-in function.
DllCallDebugger_TokenValue(token) {
    ; Implemented in machine code; see DllCallDebugger().
}

DllCallDebugger_DefaultHandler(aResultToken, aParam, aParamCount)
{
    ListLines
    
    Call = DllCall(
    Loop % aParamCount
        P:=NumGet(aParam+A_Index*4-4)
        ,V:=DllCallDebugger_TokenValue(P+0)
        ,T:=NumGet(P+8)
        ,Call.=(A_Index>1 ? ", ":"") . (T=1||T=2 ? V : """" V """")
    Call.=")"
    
    MsgBox DllCall failed with ErrorLevel = %ErrorLevel%`n`n     Specifically: %Call%
}
Example:
; Initialize/set our callback.
DllCallDebugger("OnDllCallFail")

; Should always succeed:
DllCall("MulDiv","int",1,"int",1,"int",1)
if ErrorLevel = 0
    MsgBox DllCall succeeded

; Should always fail:
DllCall("foo")
DllCall("MulDiv","int",1,"int",1)

OnDllCallFail() {
    MsgBox DllCall failed: %ErrorLevel%
}
For licensing terms, see Lexikos' default copyright license.

2009/09/27:
[*:3f23cumj]Updated for AutoHotkey_L31 compatibility.
[*:3f23cumj]Calling DebugBif() with no parameters a second time no longer attempts to re-hook the functions.
[*:3f23cumj]Added ListLines On/Off to prevent most of the debug code from appearing in ListLines.


#2 Laszlo

Laszlo
  • Fellows
  • 4713 posts

Posted 13 October 2008 - 01:39 PM

Wonderful, much needed function! Thanks for sharing it. Keep up the good work!

#3 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 15 January 2009 - 10:10 PM

Updated with default handler and to allow inspection of parameters.

#4 majkinetor

majkinetor
  • Fellows
  • 4511 posts

Posted 15 January 2009 - 11:30 PM

So nice.

Thx.

#5 Laszlo

Laszlo
  • Fellows
  • 4713 posts

Posted 16 January 2009 - 12:27 AM

Still no plans to support the latest AHK beta?

#6 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 16 January 2009 - 08:37 AM

Before I posted, I started updating LowLevel to support the beta. Apart from __expr, which will not be supported, there aren't many differences. ArgStruct has one extra field at the end (ExprTokenType *postfix) and Var has an extra union at the beginning (__int64 mContentsInt64, double mContentsDouble). Within DllCallDebugger, both machine code functions must be adjusted - Var::mContents is now at offset 8, from 0. Unfortunately, even after doing this, an access violation occurs at movsx ecx, byte ptr [ecx]. I may be missing something...

I actually updated DllCallDebugger months ago, but hadn't been motivated to post it until [the time of my previous post].

#7 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 23 January 2009 - 02:49 PM

Having updated LowLevel to support the pre-v1.0.48 beta of AutoHotkey, I've also updated my first post in this thread with a much more generic debugging function. Of course, the pre-v1.0.48 beta of AutoHotkey and appropriate version of LowLevel are required.

[Edit]

#8 Joy2DWorld

Joy2DWorld
  • Members
  • 562 posts

Posted 20 March 2009 - 01:41 AM

(just saw this). incredible.

#9 fincs

fincs
  • Fellows
  • 1532 posts

Posted 20 March 2009 - 09:07 PM

I have a question, why are you FileAppending to NUL?

#10 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 20 March 2009 - 11:54 PM

Those lines essentially do nothing, but are easily visible in ListLines.

#11 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 27 September 2009 - 01:33 AM

2009/09/27:
[*:33lwtgks]Updated for AutoHotkey_L31 compatibility.
[*:33lwtgks]Calling DebugBif() with no parameters a second time no longer attempts to re-hook the functions.
[*:33lwtgks]Added ListLines On/Off to prevent most of the debug code from appearing in ListLines.

I also disabled the FileAppend -> NUL lines since ListLines On/Off makes them unnecessary.

#12 tinku99

tinku99
  • Members
  • 560 posts

Posted 23 June 2010 - 05:21 AM

fragman recently asked if it would be possible to make richObject() behave like object().
Unfortunately, user defined functions don't support unnamed optional parameters.
Variadic functions could be simulated by calling a variadic bif (such as "dllcall") with one bogus argument plus the arguments meant for the user variadic.
dllcall(0, param1, param2...)
However, DebugBIF_DefaultHandler falls down on objects.
Also, how do you get the result of the DebugBIF_DefaultHandler back to the caller of the intentionally faulty bif ?

#13 Lexikos

Lexikos
  • Administrators
  • 8855 posts

Posted 23 June 2010 - 01:49 PM

That seems very awkward. See my most recent post in the LowLevel thread for a cleaner, more direct solution.

#14 HotKeyIt

HotKeyIt
  • Fellows
  • 6134 posts

Posted 16 November 2010 - 06:18 PM

Thanks for this Lexikos, I can't believe I have missed that, this is a must have function :oops:

#15 fincs

fincs
  • Fellows
  • 1532 posts

Posted 16 November 2010 - 08:31 PM

Or use the built-in variadic support:
myFunc(2, 3, 4)

myFunc(prm*)
{
    for k,v in prm
        msgbox %v%
}