/* (C) 2003-2007 Willem Jan Hengeveld * Web: http://www.xs4all.nl/~itsme/ * http://wiki.xda-developers.com/ * * $Id: pmemdump.cpp 1921 2008-07-31 17:08:31Z itsme $ * * todo: implement macosx version * todo: handle pagefile */ #include #include #include #ifdef WINCEMEMDMP #include "ItsUtils.h" #include "dllversion.h" #include #endif #ifdef WIN32MEMDMP #include #include "sysint-physmem.h" #endif #include "debug.h" #include "stringutils.h" #include "ptrutils.h" #include "args.h" #include #include #include #include // done: fix pmemdump -c, such that it does not stop at NUL chars // -> stdout set to binmode // bug: pmemdump -x -f -2 -w 176 0x8e001020 0x12E80 // after 0x10000 the '-w 176' is broken, should continue. // // DumpUnitType g_dumpunit=DUMPUNIT_BYTE; DumpFormat g_dumpformat= DUMP_HEX_ASCII; int g_nMaxUnitsPerLine=-1; int g_nStepSize= 0; DWORD g_blocksize= 0x10000; bool g_verbose= false; bool g_fulldump= false; bool g_showerrors= true; // this value is usually stored in the CR3 register DWORD g_pagedirOffset= 0x39000; HANDLE GetRapiProcessHandle(); void CopyProcessMemoryToFile(HANDLE hProc, DWORD dwOffset, DWORD dwLength, char *szOutfile, DWORD nDataAccess); void StepProcessMemoryToStdout(HANDLE hProc, DWORD dwOffset, DWORD dwLength, DWORD nDataAccess); void DumpProcessMemoryToStdout(HANDLE hProc, DWORD dwOffset, DWORD dwLength, DWORD nDataAccess); HANDLE ITGetProcessHandle(char *szProcessName); DWORD GetProcessSectionSlot(HANDLE hProc); DWORD GetActivePagedir(); void usage() { printf("(C) 2003-2008 Willem jan Hengeveld itsme@xs4all.nl\n"); printf("Usage: pmemdump [ -m | -p procname | -h prochandle] start length [ filename ]\n"); printf(" numbers can be specified as 0x1234abcd\n"); printf(" -1 -2 -4 : dump as bytes/words/dwords\n"); printf(" -w NUM : specify nr of words per line\n"); printf(" -s SIZE: step with SIZE through memory\n"); printf(" -a : ascdump iso hexdump\n"); printf(" -f : full - do not summarize identical lines\n"); printf(" -c : print raw memory to stdout\n"); printf(" -x : print only hex\n"); printf(" -xx : print only fixed length ascii dumps\n"); printf(" -v : verbose\n"); printf(" -i : ignore errors\n"); printf("\n"); printf(" -n NAME: view memory in the context of process NAME\n"); #ifdef WIN32MEMDMP printf(" -h PID : view memory in the context of process with PID\n"); printf(" -m : access virtual kernel memory, via Idle-Pagedir\n"); printf(" -mm : access virtual kernel memory, via active-Pagedir\n"); printf(" -mNUM : access virtual kernel memory, via specified Pagedir\n"); #else printf(" -h NUM : view memory in the context of process with handle NUM\n"); printf(" -m : directly access memory - not using ReadProcessMemory\n"); #endif printf(" -p : access physical memory, instead of virtual memory\n"); printf(" if neither -p, -h or -m is specified, memory is read from the context\n"); printf(" of rapisrv.exe\n"); printf("\n"); } int main( int argc, char *argv[]) { DebugStdOut(); DWORD dwOffset=0; DWORD dwLength=0; DWORD dwSectionBase= 0; char *szOutfile=NULL; char *szProcessName= NULL; HANDLE hProc= INVALID_HANDLE_VALUE; bool bDirectMemoryAccess= false; // false: use readprocessmemory, true: use readmemory bool bPhysicalMemoryAccess= false; // false: use readprocessmemory, true: use readmemory bool bIgnoreErrors= false; int nDataAccess= 0; int nDumpUnitSize= 1; int argsfound=0; for (int i=1 ; ii && argv[i+1][0]!='-'*/) HANDLEULOPTION(g_pagedirOffset, DWORD); #endif break; case 'p': bPhysicalMemoryAccess= true; break; case 'v': g_verbose= true; break; case 'a': g_dumpformat= DUMP_STRINGS; break; case 'c': g_dumpformat= DUMP_RAW; break; case 'x': if (argv[i][2]=='x') g_dumpformat= DUMP_ASCII; else g_dumpformat= DUMP_HEX; break; case 'f': g_fulldump= true; break; case 'w': HANDLEULOPTION(g_nMaxUnitsPerLine, DWORD); break; case 's': HANDLEULOPTION(g_nStepSize, DWORD); break; case 'b': HANDLEULOPTION(g_blocksize, DWORD); break; case '1': case '2': case '4': nDataAccess= argv[i][1]-'0'; break; default: usage(); return 1; } else switch (argsfound++) { case 0: dwOffset= strtoul(argv[i], 0, 0); break; case 1: dwLength= strtoul(argv[i], 0, 0); break; case 2: szOutfile= argv[i]; break; } } if (argsfound==0 || argsfound>3) { usage(); return 1; } if (argsfound==1) dwLength= 0x100; if (nDataAccess) nDumpUnitSize= nDataAccess; if (g_nMaxUnitsPerLine<0) { if (g_dumpformat==DUMP_ASCII) g_nMaxUnitsPerLine= 64/nDumpUnitSize; else if (g_dumpformat==DUMP_HEX) g_nMaxUnitsPerLine= 32/nDumpUnitSize; else g_nMaxUnitsPerLine= 16/nDumpUnitSize; } g_dumpunit= nDumpUnitSize==1?DUMPUNIT_BYTE: nDumpUnitSize==2?DUMPUNIT_WORD: nDumpUnitSize==4?DUMPUNIT_DWORD:DUMPUNIT_BYTE; if (g_dumpformat==DUMP_RAW) { if (-1==_setmode( _fileno( stdout ), _O_BINARY )) { error("_setmode(stdout, rb)"); return false; } } #ifdef WINCEMEMDMP CheckITSDll(); #endif if (hProc!=INVALID_HANDLE_VALUE) // -h { // - do nothing, process handle already there. #ifdef WIN32MEMDMP hProc= OpenProcess(PROCESS_ALL_ACCESS, 0, (DWORD)hProc); #endif } else if (szProcessName==NULL) // none of -m, -p, -h, -n { #ifdef WINCEMEMDMP hProc= GetRapiProcessHandle(); if (hProc==INVALID_HANDLE_VALUE) { debug("error getting process context\n"); return 1; } #else if (!bPhysicalMemoryAccess) { debug("need processname\n"); return 1; } #endif } else { // -n hProc= ITGetProcessHandle(szProcessName); if (hProc==INVALID_HANDLE_VALUE || hProc==NULL) { debug("error getting process context\n"); return 1; } } if (bDirectMemoryAccess) // -m { #ifdef WINCEMEMDMP if (hProc!=NULL && hProc!=INVALID_HANDLE_VALUE) dwSectionBase= GetProcessSectionSlot(hProc); #endif hProc= NULL; } else if (bPhysicalMemoryAccess) // -p { #ifdef WINCEMEMDMP if (hProc!=NULL && hProc!=INVALID_HANDLE_VALUE) dwSectionBase= GetProcessSectionSlot(hProc); #endif hProc= INVALID_HANDLE_VALUE; } if (g_nStepSize) StepProcessMemoryToStdout(hProc, dwOffset+dwSectionBase, dwLength, nDataAccess|(bIgnoreErrors?8:0)); else if (szOutfile==NULL) DumpProcessMemoryToStdout(hProc, dwOffset+dwSectionBase, dwLength, nDataAccess|(bIgnoreErrors?8:0)); else CopyProcessMemoryToFile(hProc, dwOffset+dwSectionBase, dwLength, szOutfile, nDataAccess|(bIgnoreErrors?8:0)); #ifdef WINCEMEMDMP CeRapiUninit(); #endif return 0; } #ifdef WINCEMEMDMP HANDLE GetRapiProcessHandle() { DWORD outsize=0; GetContextResult *outbuf=NULL; HRESULT res= ItsutilsInvoke(L"ITGetContext", 0, NULL, &outsize, (BYTE**)&outbuf); if (res || outbuf==NULL) { error(res, "ITGetContext"); return INVALID_HANDLE_VALUE; } HANDLE hProc= outbuf->hProcess; LocalFree(outbuf); return hProc; } HANDLE ITGetProcessHandle(char *szProcessName) { DWORD insize= (strlen(szProcessName)+1)*sizeof(WCHAR); WCHAR *inbuf= (WCHAR*)LocalAlloc(LPTR, insize); _snwprintf((WCHAR*)inbuf, strlen(szProcessName), L"%hs", szProcessName); inbuf[strlen(szProcessName)]= 0; DWORD outsize=0; HANDLE *outbuf=NULL; HRESULT res= ItsutilsInvoke(L"ITGetProcessHandle", insize, (BYTE*)inbuf, &outsize, (BYTE**)&outbuf); if (res || outbuf==NULL) { error(res, "ITGetProcessHandle"); return INVALID_HANDLE_VALUE; } HANDLE hproc= *outbuf; LocalFree(outbuf); return hproc; } typedef std::map ProcessInfoMap; bool GetProcessInfo(bool bIncludeHeap, ProcessInfoMap &pinfo) { GetProcessListParams p; p.bIncludeHeapUsage= bIncludeHeap; DWORD outsize=0; GetProcessListResult *outbuf=NULL; HRESULT res= ItsutilsInvoke(L"ITGetProcessList", sizeof(GetProcessListParams), (BYTE*)&p, &outsize, (BYTE**)&outbuf); if (res || outbuf==NULL) { error(res, "ITGetProcessList"); return false; } if (outsizepe) || outsize < PTR_DIFF(outbuf, &outbuf->pe[outbuf->nEntries])) { debug("INTERNAL ERROR in itsutils.dll: expected %d bytes from ITGetProcessList, got %d\n", PTR_DIFF(outbuf, &outbuf->pe[outbuf->nEntries]), outsize); return false; } for (int i=0 ; inEntries ; i++) { memcpy(&pinfo[(HANDLE)outbuf->pe[i].dwProcessID], &outbuf->pe[i], sizeof(CEPROCESSENTRY)); } LocalFree(outbuf); return true; } DWORD GetProcessSectionSlot(HANDLE hProc) { ProcessInfoMap pmap; if (GetProcessInfo(false, pmap)) if (pmap.find(hProc)!=pmap.end()) return pmap[hProc].dwMemoryBase; return 0; } bool ITReadProcessMemory(HANDLE hProc, DWORD dwOffset, BYTE *buffer, DWORD dwBytesWanted, DWORD *pdwNumberOfBytesRead, DWORD nDataAccess) { ReadProcessMemoryParams inbuf; DWORD outsize=0; ReadProcessMemoryResult *outbuf=NULL; inbuf.hProcess= hProc; inbuf.dwOffset= dwOffset; inbuf.nSize= dwBytesWanted; inbuf.nDataAccess= nDataAccess; outbuf= NULL; outsize= 0; HRESULT res= ItsutilsInvoke(L"ITReadProcessMemory", sizeof(ReadProcessMemoryParams), (BYTE*)&inbuf, &outsize, (BYTE**)&outbuf); if (res || outbuf==NULL) { if (g_showerrors) error(res, "ITReadProcessMemory"); return false; } memcpy(buffer, &outbuf->buffer, outbuf->dwNumberOfBytesRead); *pdwNumberOfBytesRead= outbuf->dwNumberOfBytesRead; LocalFree(outbuf); return true; } #elif defined(WIN32MEMDMP) HANDLE GetRapiProcessHandle() { return GetCurrentProcess(); } HANDLE ITGetProcessHandle(char *szProcessName) { HANDLE hTH= CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS/*|TH32CS_SNAPNOHEAPS*/, 0); PROCESSENTRY32 pe; pe.dwSize= sizeof(PROCESSENTRY32); HANDLE hProc= INVALID_HANDLE_VALUE; if (Process32First(hTH, &pe)) { do { if (stricmp(szProcessName, pe.szExeFile)==0) { hProc= OpenProcess(PROCESS_ALL_ACCESS, 0, pe.th32ProcessID); if (hProc != INVALID_HANDLE_VALUE && hProc!=NULL) break; } } while (Process32Next(hTH, &pe)); } #ifdef _WIN32_WCE CloseToolhelp32Snapshot(hTH); #else CloseHandle(hTH); #endif return hProc; } #define MEM_PAGE_SIZE 4096 bool ReadMemory(DWORD dwStart, BYTE *buf, DWORD dwLength, DWORD *pdwCopied, DWORD nDataAccess) { DWORD nCopied= 0; DWORD addr= dwStart; if ((nDataAccess&7)==0) { while (nCopied < dwLength) { DWORD nChunkSize= min(MEM_PAGE_SIZE-(addr&(MEM_PAGE_SIZE-1)), dwLength-nCopied); if (IsBadReadPtr((void*)addr, nChunkSize)) break; memcpy(&buf[nCopied], (void*)addr, nChunkSize); nCopied += nChunkSize; addr += nChunkSize; } } else if (IsBadReadPtr((void*)addr, dwLength)) { // no direct access. } else if ((nDataAccess&7)==1) { while (nCopied < dwLength) { *(BYTE*)(buf+nCopied)= *(BYTE*)addr; nCopied++; addr++; } } else if (addr&1) { // non-word aligned address not allowed } else if ((nDataAccess&7)==2) { while (nCopied+1 < dwLength) { *(WORD*)(buf+nCopied)= *(WORD*)addr; nCopied+=2; addr+=2; } } else if (addr&3) { // non-dword aligned address not allowed } else if ((nDataAccess&7)==4) { while (nCopied+3 < dwLength) { *(DWORD*)(buf+nCopied)= *(DWORD*)addr; nCopied+=4; addr+=4; } } *pdwCopied= nCopied; return true; } bool ReadPhysicalMemory(DWORD dwStart, BYTE *buf, DWORD dwLength, DWORD *pdwCopied, int nDataAccess) { if (!LocateNtdllEntryPoints()) return false; HANDLE physmem = OpenPhysicalMemory(); if (physmem==NULL || physmem==INVALID_HANDLE_VALUE) return false; DWORD vaddress; DWORD dwRealLength= dwLength; DWORD dwRealStart= dwStart; if (!MapPhysicalMemory( physmem, &dwRealStart, &dwRealLength, &vaddress )) return false; *pdwCopied= min(dwRealLength-(dwStart-dwRealStart),dwLength); DWORD dummy; if (!ReadMemory(vaddress+dwStart-dwRealStart, buf, *pdwCopied, &dummy, nDataAccess)) return false; UnmapPhysicalMemory( vaddress ); CloseHandle( physmem ); return true; } typedef std::vector DwordList; DwordList pagedir; std::map tablemap; bool LoadPhysicalPage(DWORD dwAddr, DwordList& pdir) { if (dwAddr&0xfff) { debug("ERROR - pagetable at unalign address\n"); return false; } pdir.resize(1024); DWORD dwRead=0; return ReadPhysicalMemory(dwAddr, (BYTE*)vectorptr(pdir), pdir.size()*sizeof(DWORD), &dwRead, 0) && dwRead==pdir.size()*sizeof(DWORD); } bool LoadPageTable(DWORD dwAddr, DwordList& pdir) { return LoadPhysicalPage(dwAddr, pdir); } bool LoadPageDirectory(DwordList& pdir) { return LoadPhysicalPage(g_pagedirOffset, pdir); } bool MapVirtualToPhysical(DWORD dwVAddr, DWORD *pdwPAddr) { if (pagedir.empty()) if (!LoadPageDirectory(pagedir)) return false; int pdi= (dwVAddr>>22)&0x3ff; DWORD pde= pagedir[pdi]; // PDE flags: // bit0 001 valid // bit1 002 // bit2 004 // bit3 008 // bit4 010 // bit5 020 // bit6 040 // bit7 080 smallpage // bit8 100 // bit9 200 // bitA 400 prototype // bitB 800 transition if ((pde&1)==0) return false; // T P V // 0 0 0 'pagefile' filenr=(pde>>1)&0xf, ofs=(pde&~0xfff)+(vaddr>>12)&0x3ff // 0 0 1 // x 1 0 'prototype', prototypeindex= (pde>>11) // x 1 1 // 1 0 0 'transition' // 1 0 1 bool isSmallPage= (pde&0x80)==0; if (isSmallPage) { DWORD dwPdeAddr= pde&~0xfff; if (tablemap[dwPdeAddr].empty()) if (!LoadPageTable(dwPdeAddr, tablemap[dwPdeAddr])) return false; int pti= (dwVAddr>>12)&0x3ff; DWORD pte= tablemap[dwPdeAddr][pti]; if ((pte&1)==0) return false; DWORD dwPteAddr= pte&~0xfff; *pdwPAddr= dwPteAddr|(dwVAddr&0xfff); return true; } else { DWORD dwPdeAddr= pde&~0x3fffff; *pdwPAddr= dwPdeAddr|(dwVAddr&0x3fffff); return true; } } bool ReadVirtualMemory(DWORD dwVAddr, BYTE *buf, DWORD dwLength, DWORD *pdwCopied, int nDataAccess) { DWORD dwPAddr; if (!MapVirtualToPhysical(dwVAddr, &dwPAddr)) return false; if ((dwPAddr&0xfff)+dwLength > 0x1000) { dwLength= 0x1000-(dwPAddr&0xfff); } return ReadPhysicalMemory(dwPAddr, buf, dwLength, pdwCopied, nDataAccess); } bool LoadVirtualData(DWORD dwAddr, DWORD dwSize, DwordList& data) { data.resize(dwSize/sizeof(DWORD)); BYTE *ptr= (BYTE*)vectorptr(data); while (dwSize) { DWORD dwRead=0; if (!ReadVirtualMemory(dwAddr, ptr, dwSize, &dwRead, 0)) return false; ptr += dwRead; dwSize -= dwRead; } return true; } DWORD GetActivePagedir() { DwordList pcr; if (!LoadPhysicalPage(0x40000, pcr)) { debug("ERROR loading PCR from physaddr 0x40000\n"); return false; } // 0x124 : struct _KTHREAD *CurrentThread; DwordList kthread; if (!LoadVirtualData(pcr[0x124/4], 0x1b8, kthread)) { debug("ERROR loading KTHREAD from physaddr 0x%08lx\n", pcr[0x124/4]); return false; } // 0x44 : struct _KPROCESS *Process; DwordList kprocess; if (!LoadVirtualData(kthread[0x44/4], 0x68, kprocess)) { debug("ERROR loading KPROCESS from physaddr 0x%08lx\n", kthread[0x44/4]); return false; } pagedir.clear(); tablemap.clear(); return kprocess[0x18/4]; // DirectoryTableBase } bool ITReadProcessMemory(HANDLE hProc, DWORD dwOffset, BYTE *buffer, DWORD dwBytesWanted, DWORD *pdwNumberOfBytesRead, DWORD nDataAccess) { if (hProc==INVALID_HANDLE_VALUE) return ReadPhysicalMemory(dwOffset, buffer, dwBytesWanted, pdwNumberOfBytesRead, nDataAccess); else if (hProc==NULL) return ReadVirtualMemory(dwOffset, buffer, dwBytesWanted, pdwNumberOfBytesRead, nDataAccess); else if (!ReadProcessMemory(hProc, (LPCVOID)dwOffset, buffer, dwBytesWanted, pdwNumberOfBytesRead)) return false; return true; } #elif defined(UNIXMEMDMP) // todo #endif void StepProcessMemoryToStdout(HANDLE hProc, DWORD dwOffset, DWORD dwLength, DWORD nDataAccess) { ByteVector buffer; std::string prevline; bool bSamePrinted= false; g_showerrors= false; while (dwLength) { buffer.resize(DumpUnitSize(g_dumpunit)*g_nMaxUnitsPerLine); DWORD dwBytesWanted= min(dwLength, buffer.size()); DWORD dwNumberOfBytesRead; std::string line; if (!ITReadProcessMemory(hProc, dwOffset, vectorptr(buffer), dwBytesWanted, &dwNumberOfBytesRead, nDataAccess)) { line= " * * * * * *\n"; // indicates invalid memory } else if (dwNumberOfBytesRead) { if (g_dumpformat==DUMP_RAW) { line.clear(); } else if (g_dumpformat==DUMP_STRINGS) line= ascdump(buffer, "\r\n\t", true); else if (g_dumpformat==DUMP_ASCII) line= asciidump(vectorptr(buffer), dwNumberOfBytesRead)+"\n"; else line= hexdump(dwOffset, vectorptr(buffer), dwNumberOfBytesRead, DumpUnitSize(g_dumpunit), g_nMaxUnitsPerLine).substr(9); } else { line= " # # # # # #\n"; // indicates 0 bytes read } if (g_dumpformat==DUMP_RAW) fwrite(vectorptr(buffer), 1, buffer.size(), stdout); else if (!g_fulldump && line == prevline) { if (!bSamePrinted && line != " * * * * * *\n") printf("*\n", dwOffset); bSamePrinted= true; } else { bSamePrinted= false; printf("%08lx: %hs", dwOffset, line.c_str()); } prevline= line; DWORD dwStep= min(dwLength, g_nStepSize); dwLength -= dwStep; dwOffset += dwStep; } g_showerrors= true; } void DumpProcessMemoryToStdout(HANDLE hProc, DWORD dwOffset, DWORD dwLength, DWORD nDataAccess) { ByteVector buffer; bool bPrevError= false; DWORD flags= hexdumpflags(g_dumpunit, g_nMaxUnitsPerLine, g_dumpformat) | (g_fulldump?0:HEXDUMP_SUMMARIZE) | (g_dumpformat==DUMP_RAW?0:HEXDUMP_WITH_OFFSET); while (dwLength) { buffer.resize(g_blocksize); DWORD dwWanted= min(dwLength, buffer.size()); DWORD dwNumberOfBytesRead; std::string line; if (!ITReadProcessMemory(hProc, dwOffset, vectorptr(buffer), dwWanted, &dwNumberOfBytesRead, nDataAccess)) { if (!bPrevError) debug("%08lx: * * * * *\n", dwOffset); dwNumberOfBytesRead= dwWanted; bPrevError= true; } else if (dwNumberOfBytesRead) { buffer.resize(dwNumberOfBytesRead); bighexdump(dwOffset, buffer, flags| (dwLength!=dwNumberOfBytesRead ? HEXDUMP_MOREFOLLOWS : 0)); bPrevError= false; } else { debug("WARNING: skipping %08lx bytes\n", dwWanted); dwNumberOfBytesRead= dwWanted; } dwLength -= dwNumberOfBytesRead; dwOffset += dwNumberOfBytesRead; } } std::string hhmmss(DWORD s) { return stringformat("%2d:%02d:%02d", s/3600, (s/60)%60, s%60); } void CopyProcessMemoryToFile(HANDLE hProc, DWORD dwStartOffset, DWORD dwLength, char *szOutfile, DWORD nDataAccess) { g_showerrors= false; debug("CopyProcessMemoryToFile(%08lx, %08lx, %08lx, %s)\n", hProc, dwStartOffset, dwLength, szOutfile); HANDLE hDest = CreateFile( szOutfile, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hDest) { error("Unable to open host/destination file"); return; } DWORD t0= GetTickCount(); DWORD tprev= t0; ByteVector buffer; DWORD dwOffset= dwStartOffset; while (dwLength) { buffer.resize(g_blocksize); DWORD dwWanted= min(dwLength, buffer.size()); DWORD dwNumberOfBytesRead; if (!ITReadProcessMemory(hProc, dwOffset, vectorptr(buffer), dwWanted, &dwNumberOfBytesRead, nDataAccess)) { // skip invalid part DWORD dwNewOffset= SetFilePointer(hDest, dwWanted, 0, FILE_CURRENT); if (dwNewOffset==INVALID_SET_FILE_POINTER) error("SetFilePointer(%08lx)", dwWanted); dwNumberOfBytesRead= dwWanted; } else { DWORD dwNumWritten; if (!WriteFile(hDest, vectorptr(buffer), dwNumberOfBytesRead, &dwNumWritten, NULL)) { error("Error Writing file"); return; } } dwLength -= dwNumberOfBytesRead; dwOffset += dwNumberOfBytesRead; DWORD t= GetTickCount(); if (g_verbose && t-tprev>2000) { double bps= (double)1000.0*(dwOffset-dwStartOffset)/(t-t0); printf("read %08lx bytes in %6d msec : %8.0f bytes/sec - timeleft: %hs\r", dwOffset-dwStartOffset, t-t0, bps, hhmmss(dwLength/bps).c_str()); tprev= t; } } DWORD t1= GetTickCount(); if (g_verbose) { printf("read %08lx bytes in %6d msec : %8.0f bytes/sec\n", dwOffset-dwStartOffset, t1-t0, (double)1000.0*(dwOffset-dwStartOffset)/(t1-t0)); } CloseHandle (hDest); g_showerrors= true; }