How to log application API calls using import module addresses

Let’s log all the calls that Excel makes to open or create a file.

Start Visual Studio (any version), choose File->Open->Projects. In the dialog, change the “Files of Type” to “Executable Files (*.exe)”

Choose any application like Excel: C:\Program Files\Microsoft Office\OFFICE11\EXCEL.EXE

Hit Ctrl-B to bring up the Breakpoints dialog. Paste in 0x7C810760 (which is the address of CreateFileW on WinXP (below). On Vista, use 0x75F2866C)

Hit F5 to start Excel

The debugger will stop when Excel calls the API. Put this expression in the Watch Window:

(char *)(*(int *)((esp+4))),su

The ESP register is the stack pointer. At the breakpoint it points to the return address of the caller. The next stack entry (esp+4) is the first parameter to the function. (There are four 8 bit bytes per 32 bit word.) The watch expression dereferences (“*(int *)”)the value and casts it as a string (“char *”)so you can see the value. The “,su” means to format it as a Unicode string. We know the first parameter is the file name from the CreateFileW documentation.

My debugger shows

 "C:\Program Files\Microsoft Office\OFFICE11\1033\xlintl32.dll"

Right click the bpt, choose BreakPoint->When Hit->Print a Message -> “CFileUnicode {(char *)(*(int *)((esp+4))),su}”

(The When Hit feature is VS 2005 only)

Now watch the output window as you run the target application.

I see these file names passed to the CreateFileW function as Excel runs:

CFileUnicode "C:\Program Files\Microsoft Office\OFFICE11\1033\xlintl32.dll"

CFileUnicode "C:\WINDOWS\WindowsShell.Manifest"

CFileUnicode "C:\WINDOWS\system32\msctfime.ime"

CFileUnicode "C:\WINDOWS\system32\msctfime.ime"

CFileUnicode "C:\WINDOWS\system32\OLEACCRC.DLL"

CFileUnicode "C:\WINDOWS\Registration\R000000000049.clb"

CFileUnicode "C:\Program Files\Common Files\Microsoft Shared\office11\1033\msointl.dll"

CFileUnicode "C:\Documents and Settings\Calvinh\Application Data\Microsoft\Office\Excel11.pip"

CFileUnicode "C:\WINDOWS\system32\tipres.dll"

CFileUnicode "\\.\PIPE\lsarpc"

CFileUnicode "\\.\PIPE\lsarpc"


CFileUnicode "C:\WINDOWS\system32\oleacc.dll"



CFileUnicode "C:\Program Files\Microsoft Office\OFFICE11\1033\id_011.dpc"

Similarly, you can spy on a program’s calls to other API functions, such as registry operations, window handling, etc. (see also Spy on your programs).

Of course, you can use one of the utilities from to monitor registry/file usage, but this is another tool in your arsenal.

For another example, suppose you use a program (perhaps Word or Excel) to create a directory. Does the program check for the existence of the dir and if it already exists, not even call the CreateDirectory API? Or does it call the API and test for failure? Put a breakpoint on the CreateDirectory Api and see.

Actually there are a few versions of the CreateDirectory APIs: CreateDirectoryA, CreateDirectoryW are the AnsiWide versions (see DECLARE DLL performance questions).

A while ago I posted Find all statically linked libraries required before your process can start (be sure to fix the code by adding “[i]”: see the comments) which showed how to list all the DLLs that an application needs before it gets loaded.

I’ve modified the program and added a feature to list the addresses of the imported functions. These addresses are useful when using a debugger as above. CreateDirectoryA has a relative address of 0x217AC. The absolute address is just the sum of the relative address and the module load address.

Run the sample code to get a table of exported functions and their addresses used by a particular application.

You can use GetModuleHandle to find the address of a module as loaded within the VFP process:

DECLARE integer GetModuleHandle IN WIN32API string


This shows kernel32 loaded at 0x7c80000

You can also use the Modules Window of the VS Debugger to get the actual module load address. For essential modules, like kernel32.dll, the load address is the same for most processes. They get loaded first, and there are no preferred load address collisions yet. As more modules get loaded, one might collide with another and must be rebased to a different load address.

Attach the debugger: start Visual Studio (any version). Hit Ctrl-Alt-P (Debug->Attach to Process) and attach to the target process (VFP or anything else that will call CreateDirectory)

0x7c80000+ 217AC = 0x7c8217ac

Put a breakpoint at the calculated address: 0x7c356eac

Make a directory using the debuggee: Type this into the command window:

MD ThisIsATestDir

As before, In the Watch window we an see the parameters of the call. Paste this in: (char *)(*(int *)((esp+4)))

+ (char *)(*(int *)((esp+4))) 0x0013fb68 "ThisIsATestDir" char *

Of course, another way to determine the function’s relative address is:

C:\Program Files\Microsoft Visual Studio 8\VC\bin>link /dump /exports c:\windows\system32\kernel32.dll | find /i "CreateDirectory"

         72 47 000217AC CreateDirectoryA

         73 48 0005B23B CreateDirectoryExA

         74 49 0005A5F2 CreateDirectoryExW

         75 4A 000323D2 CreateDirectoryW

But this way requires knowing what to search for and which DLL it resides in.

The sample program below creates table of all the imports used by all modules that are statically loaded so you can search them regardless of host module.

Yet another way to set a bpt is to use the VS syntax, such as {,,kernel32.dll}_LoadLibraryA@4 (see

However, this requires you to know the DLL name and the decorated symbol name (which can be different if you have symbols loaded or not).

To load symbols under the debugger when the process starts, use Tools->Options->Debugging->Symbols. Uncheck “Search the above locations only when the symbols are loaded manually”.

Related topics:

Very Advanced Debugging tips

Dynamically attaching a debugger

Is a process hijacking your machine?

Customize the VS debugger display of your data

MSJ Bugslayer column:

Sample Code:





SELECT exports

INDEX on funcname TAG funcname


DEFINE CLASS CModules as Custom

          nMaxDepth=20000 && recursion depth

          cVars=LOCFILE("c:\Program Files\Microsoft Visual Studio 8\Vc\bin\vcvars32.bat") && VS2005

* cVars=LOCFILE("c:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\bin\vcvars32.bat") && VS2003

          PROCEDURE init(nMaxDepth as Integer)

                   IF VARTYPE(nMaxDepth)== "N"

                             this.nMaxDepth=nMaxDepth && keep this small if you want things close by


                   DECLARE integer GetModuleFileName IN WIN32API integer hModule, string @ lpfilename, integer nSize

                   DECLARE integer LoadLibrary IN WIN32API string FileName

                   DECLARE integer FreeLibrary IN WIN32API integer hModule

                   DECLARE integer GetModuleHandle IN WIN32API string

                   CREATE CURSOR modules (module c(40),ModuleFullName c(200),Level i)

                   INDEX on UPPER(module) TAG module

                   CREATE CURSOR imports (Parent c(200), Module c(40),address c(10),FuncName c(40))

                   INDEX on FuncName+ module TAG FuncName

                   CREATE CURSOR Exports (Module c(40),address c(10),FuncName c(40),AbsAddress c(10))

                   *this.GetImports("c:\program files\internet explorer\iexplore.exe", 1)

* this.GetImports("d:\fox90\dvfp9.exe",1)


                   ?"Now geting Exports"

                   SELECT distinct ModuleFullName FROM modules INTO CURSOR distmodules

                   SCAN && for each distinct module, get the address of the imported function


                             this.GetExports(ALLTRIM(ModuleFullName ))


          PROCEDURE GetExports(cModulePath as String)


          PROCEDURE GetImports(Parent as String,nLevel as Integer)

                   LOCAL ihm,cStrnLen,Parent

                   hm = LoadLibrary(m.parent) && load/unload so we can get the full path

                   IF hm=0

                             ?"Can't load "+m.parent




                             m.Parent=LOWER(LEFT(cStr,nLen)) && now parent is full path


                             INSERT INTO modules VALUES (LOWER(JUSTFNAME(m.Parent)), m.Parent,nLevel)



          PROCEDURE CallDumpBin(cMode as String, cModulePath as String, nLevel as Integer)

                   LOCAL i,nLines,cLine,aa[1],cCurModule



                             call "<<this.cVars>>"

                             dumpbin /<<cMode>> "<<cModulePath>>" > c:\t.txt



                   !cmd /c t.bat


                   FOR i = 1 TO nLines

                             cLine=aa [i]

                             IF cLine=" Summary"



                             IF cLine = "DUMPBIN : fatal error " OR cLine=" Bound to"




                             IF !EMPTY(cLine) AND LEFT(cLine,4)=" "

* ?cLine

                                      cCurModule=this.Process&cMode.(cLine,cModulePath,cCurModule,nLevel) && use macro substitution to call processor



          PROCEDURE ProcessImports(cLine as String, cModulePath as String, cCurModule as String , nLevel as Integer)

                   LOCAL m.Parent,m.Module,m.FuncName,m.Address

                   IF SUBSTR(cLine,5)!=' '

                             cCurModule=LOWER(ALLTRIM(cline)) && cCurModule has lifetime of several lines


                             IF !SEEK(UPPER(cCurModule),"modules") AND nLevel < this.nMaxDepth && if not processed yet

                                      this.GetImports(cCurModule,nLevel+1) &&Recur! m.Module doesn't have full path



                             IF LEFT(cline,8) = SPACE(8)

                                      m.Address = GETWORDNUM(cline,1)

                                      m.FuncName = GETWORDNUM(cline,2)

                                      m.Parent = cModulePath


                                      *filter out import address table, import name table

                                      IF m.FuncName != "Import" AND "0" != m.address AND "FFFFFFFF" != m.address

                                                INSERT INTO imports FROM MEMVAR




                   RETURN cCurModule

          PROCEDURE ProcessExports(cLine as String,cModulePath as String, cCurModule as string,nLevel as Integer)


                   m.Hint = GETWORDNUM(cLine,2)


                   m.FuncName = GETWORDNUM(cLine,4)


* ?m.Hint,m.Address,m.FuncName

                   IF ""!= m.FuncName

                             IF SEEK(PADR(m.FuncName,FSIZE("funcname","imports"))+m.Module,"imports") && if this func is imported



                                      IF nHandle>0

                                                m.AbsAddress=TRANSFORM(nHandle+ VAL("0x"+m.Address) ,"@0x") &&add Rel addr to base to get Abs Addr


                                      INSERT INTO exports FROM memvar