|
Features

Monitoring Your Console's Memory Usage, Part Two
In part one I discussed the overview of our
tool, MemAnalyze, and how to make a memory dump on Xbox and PS2
consoles. In this part, we will read the memory dump, convert function
names to addresses using Map files or PDB files, then we will process
the data to create several views on the data. I will also discuss
future plans to make MemAnalyze much more powerful.
Converting return addresses to function names
Xbox
In part one, I chose not to output function names to the memory
dump, but only the return addresses. This means that if you are
using XbMemdump,
you might want to turn off the symbol information and do the converting
yourself, to make it easier to parse the memory dump. When using
symbol information in XbMemdump,
the function addresses are replaced with the names and not added.
We can turn off the symbol information simply by not supplying a
PDB path on the command line.
Before getting into detail on either map or PDB files, I will share
a few more details regarding absolute addresses and image- and section
base addresses.
Absolute address
The return addresses we store in our memory dump are all absolute
addresses.
Image base address
Image base addresses on PCs work differently then on the Xbox.
It took me quite some time to figure this out.
On a PC, you can enter a preferred base address for the image (/BASE
linker setting), though the operating system can relocate this address
when loading the image. The operating system will only relocate
the image if there is not enough space to load the image. This only
applies when loading DLLs in the same process, not when loading
a single executable. For DLLs, the image base address can be tweaked
to avoid DLL conflicts and to gain performance.
With this knowledge in mind, I checked the Xbox project settings.
There is still the /FIXED setting in the advanced link options.
This option specifies if you always want to load the image at the
preferred base address. If the operating system cannot load it at
that address, the load fails. So the /FIXED option is there, but
the /BASE setting is not. I figured the linker would default to
some static value. So, the first thing I did was look up the base
address in the MAP file that is produced by the linker (listing
1).
|
|
XyanideD
Timestamp
is 4048a4d1 (Fri Mar 05 17:03:29 2004)
Preferred
load address is 00400000
Start
Length Name
Class
0001:00000000 00887928H .textbss DATA
|
 |
 |
 |
Listing
1: The preferred load address in the map file.
|
And there it is! Obviously, the preferred load address is 0x400000.
Well, not really. There must be some PC legacy involved here. The
Xbox image loader works differently, and the 0x400000 value is not
used at all. When starting an Xbox application, multiple modules
are loaded into the private virtual address space. Other modules,
including the kernel and debugging modules, are also loaded. Unlike
the PC platform, there is just one process running at a time, so
all modules are loaded into the same address space. The image base
address is completely determined by the Xbox image loader, which
is also why the /BASE setting is not present in the link options.
We can retrieve the image base address as described in part one,
by calling DmWalkLoadedModules,
in the game. An example of the output of DmWalkLoadedModules
is shown in listing 2.
In listing 2, we can also see that the system modules are loaded
in the space above 2GB--the shared address space. Our own module
is loaded near the beginning of the virtual address space. (The
first 0x10000 bytes of the virtual address space is reserved by
the operating system.)
Section address
Each image consists of a set of sections. These are both DATA and
CODE sections. The .text section will contain all your game code.
A map file displays a list of all the sections in the image (listing
3).
Section addresses are absolute addresses, 32-byte aligned. Figure
1 shows the layout of the virtual memory if a program is loaded
into memory.
Note: the image base address of the game module is never 0x10000,
but a couple of hundreds or thousands bytes further in memory (see
Listing 2). (This block is not displayed in Figure 1; I have not
been able to find out what data resides there.) Also, the start
of the first section is never the same as the start of the image
base address. In Figure 1, I have displayed this as "Image
information". Most likely, this is where the section headers
will reside, as the sections themselves are contiguous (mind the
section alignment though).
Map files
Map files store their functions in relative virtual addresses.
The first question that pops up is: "Relative to what?"
Well, map files store their functions along with two relative addresses:
- Section-relative.
- Relative to the image base (the column header says rva+base,
but we will see why this is incorrect in a moment).
First, we will discuss section-relative conversion.
If we take a look at a line of text from a map file (listing 4),
column 1 displays the section index and the offset into the section.
Column 2 displays the function's decorated name and column 3 displays
the offset of the function, relative to the image base. The rest
is of no use to us.
To compute the absolute address of the function displayed in listing
3, we need the start address of the section. The section index is
in front of the section relative address. In listing 4, this is
0002. Unfortunately, the section base addresses are not present
in the map file. Don't be thrown off by the list on top of the map
file--it does not display the section base addresses. We need to
parse the game's executable using ImageBld
to obtain section information. We dumped our Xyanide section information
on the command line by typing:
ImageBld /dump
XyanideD.xbe >XyanideSectionDump.txt
Now we need to parse the section dump (listing 5), find the start
address, and add it to the offset of the function. This gives us
the absolute start address of the function, but we stored the return
addresses! So we first have to compute the size of the function.
The map file displays all the functions in order of appearance in
memory, so we can simply subtract the address of our function from
the address of the next function in the map file.
|
|
SECTION
HEADER #2 .text
884140 virtual address
F21C18 virtual size
2000 file pointer to raw data
F21C18 size of raw data
000107D2 head shared page reference count address
000107D4 tail shared page reference count address
6 flags
Preload
Executable
|
 |
 |
 |
Listing
5: Layout of a section of the ImageBld.exe
tool. We need to parse the 'virtual address' field.
|
We have now parsed the section dump from ImageBld,
but we could also have used a function from the debug library on
the Xbox itself: DmWalkModuleSections.
We can save the module section addresses in the memory dump from
within the game, just as we did by saving the image base.
To convert the decorated function name to a readable name, use
the helper function UnDecorateSymbolName
from the DbgHelp library.
Now we know how to convert a function address to a function name.
There are two drawbacks to using section relative conversion if
you are using ImageBld
to parse the executable:
- If the image base address is relocated by the operating system,
the section base addresses are incorrect, because they are provided
as absolute addresses. (This code assumes that the default image
base is being used.) This has not happened to me yet, but in theory,
it is possible.
- We need to parse two files to find the function name.
By using DmWalkModuleSections,
you will only have to parse the MAP file, and it has the advantage
that the kernel knows if the image base has been relocated, so the
correct absolute addresses are written to file.
Another approach is to use the third column of the map file. This
is the image-base relative conversion. The header of the column
says this is the rva+base address, meaning it is the absolute address
of the function name. Well, that is simply not true. This code also
uses the preferred base address of 0x400000. We've just seen that
this value is not used, and incorrect. However, we can use it to
our advantage! By simply subtracting 0x400000 from this value, we
get the address relative to the image base. We can work with image
base relative values, because they are independent of the location
of the image in memory. So, regardless of whether the OS has relocated
the base address, we can find our function name. We only need to
use the memory dump's runtime image base address. We already saved
this value in part one. Thus, this is how you convert the rva+base
column to an absolute address:
AbsFunctionAddress
= RvaBase - 0x400000 + RealImageBase
Now we get to the final drawback of map files: Xbox map files do
not include static functions! This will sometimes result in incorrect
function names. They are usually not completely off; the resulting
function name will probably be a function of the same object file,
so we are pointed in the right direction. But, we could do even
better. We can read Program Databases!
PDB files
Program Databases store the functions in image base relative addresses,
so no problems there. The PDB file holds all the information you
could possibly think off. If we try hard enough, we can even double-click
a callstack item in the tool and display the function and the exact
line of code that matches the allocation.
We can parse PDB files using the DbgHelp
library and by using the DIA SDK.
Note: The Microsoft DIA SDK is an SDK that parses symbol information
from PDB files. I have had a really hard time figuring out how to
use the SDK, particularly because there is so little documentation
on it. I have asked Microsoft about this, and they told me that
version 8.0 of Visual Studio will contain a much more comprehensive
documentation.
I have stripped and adjusted the dia2dump sample that comes with
the SDK. The code from listing 6 will dump absolute function addresses,
function names and sizes, based on a given image base address.
|
|
#include "stdafx.h"
#include "diacreate.h"
#include "cvconst.h"
CComPtr<IDiaSession>
psession;
CComPtr<IDiaSymbol> pglobal;
void Fatal(
const char *msg )
{
printf( msg );
printf( "\n" );
exit(-1);
}
void Dump(
char* szFilename,
IDiaDataSource* pSource,
wchar_t* szLookup,
DWORD ImageBaseAddress
)
{
HRESULT hr;
wchar_t wszFilename[_MAX_PATH];
mbstowcs(wszFilename, szFilename,
sizeof(wszFilename) /sizeof( wszFilename[0]));
if(FAILED(pSource->loadDataFromPdb(wszFilename)))
{
if(FAILED(pSource->loadDataForExe(wszFilename,
NULL, NULL)))
{
Fatal(
"loadDataFromPdb/Exe" );
}
}
if(FAILED(pSource->openSession(&psession)))
{
Fatal("openSession");
}
if(FAILED(psession->get_globalScope(&pglobal)))
{
Fatal("get_globalScope");
}
DWORD id = 0;
pglobal->get_symIndexId(&id);
if(id == 0)
{
Fatal( "get_indexId"
);
}
psession->put_loadAddress(ImageBaseAddress);
ULONG
celt = 0;
CComPtr<IDiaEnumSymbols>
pEnum;
CComPtr<IDiaSymbol> pSymbol;
pglobal-
>findChildren(SymTagFunction,NULL,nsfCaseInsensitive
|nsfUndecoratedName,&pEnum);
while(SUCCEEDED(hr = pEnum->Next(1, &pSymbol,
&celt))
&& celt == 1)
{
BSTR name;
if(pSymbol->get_name(&name)
!= S_OK)
{
Fatal("get_name");
}
printf("Function:
%ws\n", name);
ULONGLONG
address;
pSymbol->get_virtualAddress(&address);
if(address == 0)
{
printf("
could not get address!\n");
}
printf(" Address:
%08X\n", address);
ULONGLONG
len = 0;
pSymbol->get_length(&len);
if(len == 0)
{
printf("
could not get length!\n");
}
printf(" Length:
%ld\n", len);
pSymbol
= 0;
}
}
DWORD GetImageBaseAddress()
{
// I've hardcoded the image base address
here. Replace // this by proper code
that reads the image base address // from
the memory dump.
return 0x10CE0;
}
int main(int
argc, char* argv[])
{
if(argc < 2)
{
printf( "usage: %s
<pdb-filename>\n", argv[0] );
return -1;
}
HRESULT
hr;
hr = CoInitialize(NULL);
if (FAILED(hr))
{
Fatal("CoInitialize
failed\n");
}
CComPtr<IDiaDataSource>
pSource;
// Initialize The Component Object Module
Library
// Obtain Access To The Provider
hr = CoCreateInstance( CLSID_DiaSource,
NULL, CLSCTX_INPROC_SERVER,
__uuidof( IDiaDataSource ), (void **) &pSource);
if
(FAILED(hr))
{
Fatal("Could not CoCreate
CLSID_DiaSource. Register msdia71.dll."
);
}
Dump(argv[1],
pSource, NULL, GetImageBaseAddress());
pglobal
= 0;
psession = 0;
pSource = 0;
CoUninitialize();
return
0;
}
|
 |
 |
 |
Listing
6: Parsing symbol information using the DIA SDK.
|
The DbgHelp library,
on the other hand, is very easy to use, and its documentation is
very comprehensive. Listing 7 does almost the same as the sample
code from listing 6, but using the
DebugHlp library. Keep in mind that if you are not running
on Windows XP, you need the latest DDK DLLs to run this code. These
can be found at http://www.microsoft.com/ddk/debugging.
For more information on DbgHelp,
see the DbgHelp documentation.
|
|
#include <stdafx.h>
// Just for this sample, I use a big buffer to store the
// text in (no testing for boundaries!)
char g_BigBuf[1024*1024*4] = { '\0' };
DWORD64 GetImageBaseAddress()
{
// Again, hardcoded image base address. Rewrite this to
// proper image base code.
return 0x10CE0;
}
BOOL CALLBACK EnumerateSymbolsProc(
PSYMBOL_INFO pSymInfo,
ULONG SymbolSize,
PVOID UserContext)
{
if(pSymInfo->Flags | SYMFLAG_FUNCTION)
{
char intbuf[64];
strcat(g_BigBuf, "Function: ");
strcat(g_BigBuf, pSymInfo->Name);
strcat(g_BigBuf, " Address: ");
_itoa(pSymInfo->Address, intbuf, 16);
strcat(g_BigBuf, intbuf);
strcat(g_BigBuf, " Size: ");
_itoa(SymbolSize, intbuf, 10);
strcat(g_BigBuf, intbuf);
strcat(g_BigBuf, "\n");
}
return TRUE;
}
bool SetupDbgHelp(
HANDLE hProcess
)
{
bool bResult = false;
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
if(SymInitialize(hProcess, NULL, FALSE))
{
bResult = true;
}
return bResult;
}
int GetPDBFileSize(const char* fileName)
{
HANDLE hFile = CreateFile(fileName,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
int size = GetFileSize(hFile, NULL);
CloseHandle(hFile);
return size;
}
bool OpenPDB(
HANDLE hProcess,
const char* pFileName
)
{
bool bResult = false;
DWORD64 dwBaseAddr = GetImageBaseAddress();
int fileSize = GetPDBFileSize(pFileName);
if(SymLoadModule64(
hProcess,
NULL,
pFileName,
NULL,
dwBaseAddr,
fileSize))
{
if(SymEnumSymbols(
hProcess,
dwBaseAddr,
"",
EnumerateSymbolsProc,
NULL))
{
printf(g_BigBuf);
bResult = true;
}
SymUnloadModule64(hProcess, dwBaseAddr);
}
return bResult;
}
bool ParsePDB(
const char* pFileName
)
{
HANDLE hProcess;
bool bResult = false;
hProcess = GetCurrentProcess();
if(SetupDbgHelp(hProcess))
{
if(OpenPDB(hProcess, pFileName))
{
bResult = true;
}
SymCleanup(hProcess);
}
return bResult;
}
int main(
int argc,
char* argv[]
)
{
int retCode = -1;
if(argc != 2)
{
printf("Usage: ParsePDB_DbgHelp <pdb-file>\n");
}
else
{
if(ParsePDB(argv[1]))
{
retCode = 0;
}
else
{
DWORD error = GetLastError();
printf("\nDbgHelp returned error : %d\n", error);
}
}
return retCode;
}
|
 |
 |
 |
Listing 7: Parsing symbol information using
the DbgHelp
library.
|
______________________________________________________
|