25.1 UnhandledExceptionFilter函数详解
25.1.1 BaseProcessStart伪代码(Kernel32内部)
void BaseProcessStart(PVOID lpfnEntryPoint) //参数为线程函数的入口地址 { DWORD retValue; DWORD currentESP; DWORD exceptionCode; currentESP = ESP; //lpfnEntryPoint被try/except封装着,这是系统安装的默认的异常处理程序,也是SEH链上最后一个异常处理程序 __try { NtSetInformationThread(GetCurrentThread(), ThreadQuerySetWin32StartAddress, &lpfnEntryPoint, sizeof(lpfnEntryPoint)); retValue = lpfnEntryPoint(); ExitThread(retValue); //如果异常,线程从这里退出! } __except ( //过滤器表达式代码 exceptionCode = GetExceptionInformation(), UnhandledExceptionFilter(GetExceptionInformation())) //出现异常会调用Unhandled...这个函数,该函数内部会调用
//用户通过SetUnhandledFilter设置的全局异常处理函数。 { //如果UnhandledExceptionFilter返回EXCEPTION_EXECUTE_HANDLER,则会控制流会执行到这里 ESP = currentESP;
if (!_BaseRunningInServerProcess) //普通进程,则退出进程 ExitProcess(exceptionCode); else // 线程是作为服务来运行的,只退出线程并不终止整个服务 ExitThread(exceptionCode); } }
(1)如果异常过滤程序返回EXCEPTION_CONTINUE_SEARCH时,系统会继续向外层寻找异常过滤程序。但如果每个异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH时,会未到遇处理异常。
(2)调用SetUnhandledExceptionFilter安装用户提供的全局(顶层)异常过滤回调函数(为所有线程共享)。如果顶层异常回调函数返回EXCEPTION_EXECUTE_HANDLER或EXCEPTION_CONTINUE_SEARCH则直接传递给UnhandledExceptionFilter函数,UnhandledExceptionFilter根据这个返回值判断是终止进程还是重新执行异常代码。如果顶层异常回调函数返回EXCEPTION_CONTINUE_SEARCH,则接下来的要发生的事情就比较复杂(可参考后面的《UnhandledExceptionFilter内部工作流程》)
(3)SetUnhandledExceptionFilter返回值为上次安装的异常过滤程序的地址。如果使用C/C++运行库,则会默认安装一个__CxxUnhandledExceptionFilter过滤程序。该函数首先检查异常是不是C++异常,如果是则在结束时执行abort函数(该函数内部调用了UnhandledExceptionFilter函数,注意这可能会造成循环调用,因为UnhandledExceptionFilter内部调用了我们安装的全局异常过滤函数_CxxUnhandledExceptionFilter,而这个函数的内部又调用UnhandledExceptionFilter,为了防止无限递归调用,_CxxUnhandledExceptionFilter在调用UnhandledExceptionFilter之前会调用SetUnhandledExceptionFilter(NULL))。如果不是C++异常则返回EXCEPTION_CONTINUE_SEARCH。所以当我们调用SetUnhandled*函数,返回的地址为_CxxUnhandledExceptionFilter的地址。
(4)注意,在我们的顶层异常过滤函数里,在返回EXCEPTION_CONTINUE_SEARCH前,不应调用之前的全局异常过滤函数(即我们通过SetUnhandledExceptionFilter的返回值取得的那个函数)。因为如果这个函数是在某个动态库里,那它随时都可能被卸载了。
(5)如果SetUnhandledExceptionFilter(NULL),则取消我们设置的全局异常过滤函数。
【UnhandledExceptionFilter程序】演示设置顶层异常过滤函数
#include <tchar.h> #include <windows.h> #include <locale.h> LONG WINAPI MyUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *lpTopLevelExceptionFilter) { _tprintf(_T("发生未处理异常 ")); _tsystem(_T("PAUSE")); return EXCEPTION_EXECUTE_HANDLER; //这样返回,进程将被终止。 } int _tmain() { SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); //安装用户自定义的未处理异常 _tsetlocale(LC_ALL, _T("chs")); __try{ //SetErrorMode(SEM_NOGPFAULTERRORBOX); *(int*)0 = 5;//引发异常 } __except (EXCEPTION_CONTINUE_SEARCH){ //这里返回EXCEPTION_CONTINUE_SEARCH,异常就会到达MyUnhandled* } _tsystem(_T("PAUSE"));//这行不会被执行! return 0; }
25.1.2 UnhandledExceptionFilter内部工作流程
①判断是否因为对资源进行写入操作引发的异常。如果是,将资源的只读属性改为可写入,并返回EXCEPTION_CONTINUE_EXECUTION以允许失败的指令再次执行。
②确定进程是否被调试。如果被调试,就返回EXCEPTION_CONTINUE_SEARCH给调试器,通知调试器定位异常指令,并告知我们出了什么样的异常。
③调用我们设置的顶层异常过滤函数(如果存在的话)。如果顶层过滤函数返回EXCEPT_EXECUTE_HANDLER或EXCEPTION_CONTINUE_EXECUTION,将直接传递给UnhandledExceptionFilter,由它将返回值给系统。如果返回EXCEPT_CONTINUE_SEARCH,则跳到第④步。
④再次将未处理异常报告给调试器
⑤终止进程:如果线程调用SetErrorMode并设置SEM_NOGPFAULTERRORBOX标志,那么UnhandledExceptFilter会返回EXCEPTION_EXECUTE_HANDLER,在未处理异常的情况下进行全局展开并执行未执行的finally块,然后进程终止。
如果没有调用SetErrorMode函数,UnhandledExceptionFilter会返回EXCEPTION_CONTINUE_SEARCH。于是系统内核得到程序控制,它将通过ALPC(高级本地过程调用)机制将异常通知给WerSvc(Windows错误报告专用服务),然后ALPC先阻塞自己的线程,直到WerSvc执行完毕。
⑥UnhandledExceptionFilter与WER的交互
当WerSvc接到通知时,会先调用CreateProcess来启动WerFault.exe,然后 WerSvc会等待这个新进程的结束。而WerFault.exe会向我们创建上面的两个对话框以报告错误的发生。当第1个对话框出现时,可以选择“取消”来终止我们的应用程序,否则过一会儿,会弹出第2个对话框,如果我们选择“关闭程序”,则WerFault.exe会调用TerminateProcess来结束我们的应用程序。如果选择“调试”,WerFault.exe会创建一个子进程(调试器),让他附着在出错的程序上进行“即时调试”
25.2 即时调试
(1)默认调试器:HKLMSOFTWAREMicrosoftWindows NTCurrentVersionAeDebug子项下有一个名为Debugger的值,系统通过个值找到调试器。
(2)WerFault.exe会给这个调试器传入两个参数:要调试的进程ID和继承过来的事件句柄(这个句柄由WerSvc服务创建用于通知被调试进程调试也结束)
(3)通过将调试器附着到被调试进程,可以查看全局、局部和静态变量的值,也可以设置断点,检查函数调用树等调试工作。
【Spreadsheet程序】通过SEH向预订的地址空间稀疏调拨存储器
/************************************************************************ Module: Spreadsheet.cpp Notices:Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #include "resource.h" #include "VMArray.h" #include <tchar.h> #include <strsafe.h> ////////////////////////////////////////////////////////////////////////// HWND g_hWnd; //全局的窗口句柄,SEH报告中会用到 const int g_nNumRows = 256; const int g_nNumCols = 1024; //声明单个单元格内容的结构体,每个单元格大小为1024字节 typedef struct{ DWORD dwValue; BYTE bDummy[1020]; }CELL,*PCELL; //声明全个电子表格的数据 ////SPREADSHEET类型为一个数组类型,元素类型为CELL及g_nNumRows行g_nNumCols列。 //判读时,可去掉typedef来看。 typedef CELL SPREADSHEET[g_nNumRows][g_nNumCols]; typedef SPREADSHEET* PSPREADSHEET; ////////////////////////////////////////////////////////////////////////// //一个电子表格是一个二维数组的CELLs class CVMSpreadsheet :public CVMArray<CELL>{ public: CVMSpreadsheet() :CVMArray<CELL>(g_nNumRows*g_nNumCols){} private: LONG OnAccessViolation(PVOID pvAddressTouched, BOOL bAttemptedRead, PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful); }; ////////////////////////////////////////////////////////////////////////// LONG CVMSpreadsheet::OnAccessViolation(PVOID pvAddressTouched, BOOL bAttemptedRead, PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful){ TCHAR sz[200]; StringCchPrintf(sz, _countof(sz), TEXT("非法访问:试图在0x%8X进行%s操作!"),pvAddressTouched, bAttemptedRead ? TEXT("读取") : TEXT("写入")); SetDlgItemText(g_hWnd, IDC_LOG, sz); LONG lDispostion = EXCEPTION_EXECUTE_HANDLER; //只有写入操作发生异常时才会提交物理存储器,读取操作则不会 if (!bAttemptedRead){ //返回基类的返回值 lDispostion = CVMArray<CELL>::OnAccessViolation(pvAddressTouched, bAttemptedRead, pep, bRetryUntilSuccessful); } return (lDispostion); } ////////////////////////////////////////////////////////////////////////// //产生一个全局CVMSpreadsheet对象 static CVMSpreadsheet g_ssObject; //创建一个全局指针,指向电子表格的区域 SPREADSHEET& g_ss = *(PSPREADSHEET)(PCELL)g_ssObject; ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam){ chSETDLGICONS(hWnd, IDI_SPREADSHEET); g_hWnd = hWnd; //保存句柄(For SEH错误报告) //设置对话框上面控件的默认值 Edit_LimitText(GetDlgItem(hWnd, IDC_ROW), 3); Edit_LimitText(GetDlgItem(hWnd, IDC_COLUMN), 4); Edit_LimitText(GetDlgItem(hWnd, IDC_VALUE), 7); SetDlgItemInt(hWnd, IDC_ROW, 100, FALSE); SetDlgItemInt(hWnd, IDC_COLUMN, 100, FALSE); SetDlgItemInt(hWnd, IDC_VALUE, 12345, FALSE); return (TRUE); } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hWnd, int id, HWND hwndCtrl, UINT codeNotity){ int nRow, nCol; switch (id) { case IDCANCEL: EndDialog(hWnd, id); break; case IDC_ROW: //用户修改了行数,更新UI nRow = GetDlgItemInt(hWnd, IDC_ROW, NULL, FALSE); EnableWindow(GetDlgItem(hWnd, IDC_READCELL), chINRANGE(0, nRow, g_nNumRows - 1)); EnableWindow(GetDlgItem(hWnd, IDC_WRITECELL), chINRANGE(0, nRow, g_nNumRows - 1)); break; case IDC_COLUMN: //用户修改了行数,更新UI nCol = GetDlgItemInt(hWnd, IDC_COLUMN, NULL, FALSE); EnableWindow(GetDlgItem(hWnd, IDC_READCELL), chINRANGE(0, nCol, g_nNumCols - 1)); EnableWindow(GetDlgItem(hWnd, IDC_WRITECELL), chINRANGE(0, nCol, g_nNumCols - 1)); break; case IDC_READCELL: //尝试从用户选择的单元格中读取一个数据 SetDlgItemText(hWnd, IDC_LOG, TEXT("没有发生异常!")); nRow = GetDlgItemInt(hWnd, IDC_ROW, NULL, FALSE); nCol = GetDlgItemInt(hWnd, IDC_COLUMN, NULL, FALSE); __try{ SetDlgItemInt(hWnd, IDC_VALUE, g_ss[nRow][nCol].dwValue, FALSE); } //ExceptionFilter返回EXECUTION_CONTINUE_EXECUTE或EXCEPTION_EXECUTE_HANDLER //如果提交成功,返回前者;失败,返回后者 __except (g_ssObject.ExceptionFilter(GetExceptionInformation(),FALSE)){ //单元格不支持存储,里面不含内容 SetDlgItemText(hWnd, IDC_VALUE, TEXT("")); } break; case IDC_WRITECELL: //尝试向用户选择的单元格中写入数据 SetDlgItemText(g_hWnd, IDC_LOG, TEXT("没有发生异常!")); nRow = GetDlgItemInt(hWnd, IDC_ROW, NULL, FALSE); nCol = GetDlgItemInt(hWnd, IDC_COLUMN, NULL, FALSE); //假如单元格不支持存储,将抛出非法内存访问,这时将导致自动提交存储器 //这里不设置try/except,则异常会让全局异常(未处理)过滤函数捕获 g_ss[nRow][nCol].dwValue = GetDlgItemInt(hWnd, IDC_VALUE, NULL, FALSE); break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg) { chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand); } return (FALSE); } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nShowCmd){ DialogBox(hInstance, MAKEINTRESOURCE(IDD_SPREADSHEET), NULL, Dlg_Proc); return (0); }
//VMArray.h
/************************************************************************ Module: VMArray.h Notices:Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #pragma once #include "../../CommonFiles/CmnHdr.h" #include <tchar.h> #ifndef _M_IX86 #error "The following code only works for x86!" #endif ////////////////////////////////////////////////////////////////////////// //注意:这个C++类是线程不安全的。不能在多线程下同时创建和销毁该类的实例 //但是一旦创建,多线程可同时访问不同的CVMArray对象,也可以通过自己同步的 //方法在多线程下访问同一个CVMArray对象 ////////////////////////////////////////////////////////////////////////// template <class TYPE> class CVMArray{ public: //为数组各元素预订稀疏的地址空间 CVMArray(DWORD dwreservElements); //释放 virtual ~CVMArray(); //允许访问数组中的一个元素 operator TYPE*(){ return (m_pArray); } operator const TYPE*()const { return (m_pArray); } //若提交失败,可以被优雅的处理 LONG ExceptionFilter(PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful = FALSE); protected: //虚函数,当非法访问内存时,可优雅的处理 virtual LONG OnAccessViolation(PVOID pvAddressTouched, BOOL bAttemptedRead, PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful); private: static CVMArray* sm_pHead; //第一个CVMArray对象 CVMArray* m_pNext; //下一个VCMArray对象 TYPE* m_pArray; //指向一个预订的区域数组 DWORD m_cbReserve; //预订的数组空间的大小 private: //访问前一个未处理异常过滤函数 static PTOP_LEVEL_EXCEPTION_FILTER sm_pfnUnhandledExceptionFilterPrev; //当这个类发生异常,调用我们自己的全局异常过滤函数(VS2005以后微软让 // 对CRT (C 运行时库)的一些与安全相关的代码做了些改动,使得许多错误 //都不能在SetUnhandledExceptionFilter 捕获到。 static LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pep); void DisableSetUnhandledExceptionFilter();//使SetUnhandledExceptionFilter函数失效 //为了达到设置全局异常过滤函数的目的,用向量化 //AddVectoredContinueHandler来达到设置全局异常过滤函数相同的功能 //static LONG WINAPI LastVEHandler(PEXCEPTION_POINTERS pep); //static PVOID sm_pVEH; }; ////////////////////////////////////////////////////////////////////////// //向量化异常过滤函数句柄 //template <class TYPE> //PVOID CVMArray<TYPE>::sm_pVEH = NULL; //CVMArray对象链表的头 template <class TYPE> CVMArray<TYPE>* CVMArray<TYPE>::sm_pHead = NULL; //前一个全局异常过滤函数 template <class TYPE> PTOP_LEVEL_EXCEPTION_FILTER CVMArray<TYPE>::sm_pfnUnhandledExceptionFilterPrev; ////////////////////////////////////////////////////////////////////////// template <class TYPE> CVMArray<TYPE>::CVMArray(DWORD dwreservElements){ if (sm_pHead == NULL){ //在创建第1个对象前,安装我们的全局异常过滤函数 sm_pfnUnhandledExceptionFilterPrev = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); DisableSetUnhandledExceptionFilter();//使SetUnhandledExceptionFilter失效 //sm_pVEH = AddVectoredContinueHandler(0, LastVEHandler); } m_pNext = sm_pHead; //下一次节点初始化为链表头部 sm_pHead = this; //本对象为链表头 m_cbReserve = sizeof(TYPE)*dwreservElements; //预订整个数组大小的一块区域 m_pArray = (TYPE*)VirtualAlloc(NULL, m_cbReserve, MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE); chASSERT(m_pArray != NULL); } ////////////////////////////////////////////////////////////////////////// template <class TYPE> CVMArray<TYPE>::~CVMArray(){ //释放数组所占的空间 VirtualFree(m_pArray, 0, MEM_RELEASE); //删除链表 CVMArray* p = sm_pHead; if (p == this){ sm_pHead = p->m_pNext; //删除链表头 }else{ BOOL bFound = FALSE; //遍历链头,并修复指针 for (; !bFound && (p->m_pNext != NULL);p= p->m_pNext){ if (p->m_pNext == this){ p->m_pNext = p->m_pNext->m_pNext; bFound = TRUE; break; } } chASSERT(bFound); } //if (sm_pVEH != NULL) // RemoveVectoredExceptionHandler(sm_pVEH); } ////////////////////////////////////////////////////////////////////////// //当非法访问时,默认的异常处理 template <class TYPE> LONG CVMArray<TYPE>::OnAccessViolation(PVOID pvAddressTouched, BOOL bAttemptedRead, PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful) { BOOL bCommittedStorage = FALSE; //假定提交失败 do{ //提交存储器 bCommittedStorage = (NULL != VirtualAlloc(pvAddressTouched, sizeof(TYPE),MEM_COMMIT,PAGE_READWRITE)); //假如无法提交而我们又试图重试,提醒用户释放内存 if (!bCommittedStorage && bRetryUntilSuccessful) MessageBox(NULL, TEXT("请关闭一些其他应用,然后按“确定”!"), TEXT("内存空间不足"),MB_ICONWARNING | MB_OK); } while (!bCommittedStorage && bRetryUntilSuccessful); //当提交存储器,重新执行出错代码。否则执行异常处理程序 return (bCommittedStorage ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_EXECUTE_HANDLER); } ////////////////////////////////////////////////////////////////////////// //过滤函数被关联到单一的CVMArray对象 template <class TYPE> LONG CVMArray<TYPE>::ExceptionFilter(PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful /* = FALSE */){ //默认,提交给其它过滤函数处理(这是一个安全的选择) LONG lDispostion = EXCEPTION_CONTINUE_SEARCH; //只修改非法访问内存的异常 if (pep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) return (lDispostion); //获取试图访问的地址,以及读或写异常 //对于EXCEPTION_ACCESS_VIOLATION异常,ExceptionInformation[0]指出非法访问的类型 //0表示线程试图读取不能访问的数据;1表示写入不能访问的数据 PVOID pvAddrTouched = (PVOID)pep->ExceptionRecord->ExceptionInformation[1];//非法访问的地址 BOOL bAttempedRead = (pep->ExceptionRecord->ExceptionInformation[0] == 0); //非法访问的类型 //如果试图访问的地址在VMArray的预订的地址空间内 if ((m_pArray <=pvAddrTouched) && (pvAddrTouched<((PBYTE)m_pArray + m_cbReserve))){ //访问这个数组,并尝试解决问题 lDispostion = OnAccessViolation(pvAddrTouched, bAttempedRead, pep, bRetryUntilSuccessful); } return (lDispostion); } ////////////////////////////////////////////////////////////////////////// //template <class TYPE> //LONG CVMArray<TYPE>::LastVEHandler(PEXCEPTION_POINTERS pep){ // //默认为让其他过滤器处理 // LONG lDispostion = EXCEPTION_CONTINUE_SEARCH; // // MessageBox(NULL, TEXT("发生未处理异常"), TEXT("提示"), MB_OK); // // //只修改非法访问内存 // if (pep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION){ // //遍历所有链表节点 // for (CVMArray* p = sm_pHead; p != NULL; p = p->m_pNext){ // //询问该节点是否可以修复错误 // //注意:这个错误必须被修复,否进程会被终止 // lDispostion = p->ExceptionFilter(pep, TRUE); // // //如果修复了错误,就停止循环 // if (lDispostion != EXCEPTION_CONTINUE_SEARCH) // break; // } // } // return (lDispostion); //} //新版本的CRT 实现在异常处理中强制删除所有应用程序先前设置的捕获函数,如下所示: ///* Make sure any filter already in place is deleted. */ //SetUnhandledExceptionFilter(NULL); //UnhandledExceptionFilter(&ExceptionPointers); //解决方法是拦截CRT 调用SetUnhandledExceptionFilter 函数,使之无效 template <class TYPE> void CVMArray<TYPE>::DisableSetUnhandledExceptionFilter() { void *addr = (void*)GetProcAddress(LoadLibrary(_T("kernel32.dll")), "SetUnhandledExceptionFilter"); if (addr) { unsigned char code[16]; int size = 0; code[size++] = 0x33; code[size++] = 0xC0; code[size++] = 0xC2; code[size++] = 0x04; code[size++] = 0x00; DWORD dwOldFlag, dwTempFlag; VirtualProtect(addr, size, PAGE_READWRITE, &dwOldFlag); WriteProcessMemory(GetCurrentProcess(), addr, code, size, NULL); VirtualProtect(addr, size, dwOldFlag, &dwTempFlag); } } ////////////////////////////////////////////////////////////////////////// //全局异常过滤函数,为所有CVMArray对象共用 //这个未处理异常很有必须,如果用户忘记用try/except来处理此类对象发生的异常,可以在这里 //进行最后的处理! template <class TYPE> LONG WINAPI CVMArray<TYPE>::MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pep){ //默认为让其他过滤器处理 LONG lDispostion = EXCEPTION_CONTINUE_SEARCH; //只修改非法访问内存 if (pep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION){ //遍历所有链表节点 for (CVMArray* p = sm_pHead; p != NULL;p=p->m_pNext){ //询问该节点是否可以修复错误 //注意:这个错误必须被修复,否进程会被终止 lDispostion = p->ExceptionFilter(pep, TRUE); //如果修复了错误,就停止循环 if (lDispostion != EXCEPTION_CONTINUE_SEARCH) break; } } //如果节点修复错误,试图调用前一个异常处理来处理 if (lDispostion == EXCEPTION_CONTINUE_SEARCH) lDispostion = sm_pfnUnhandledExceptionFilterPrev(pep); return (lDispostion); } ///////////////////////////////////文件结束///////////////////////////////
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 25_Spreadsheet.rc 使用 // #define IDD_SPREADSHEET 1 #define IDC_LOG 101 #define IDI_SPREADSHEET 102 #define IDI_ICON1 102 #define IDC_ROW 1001 #define IDC_COLUMN 1002 #define IDC_COLUMN2 1003 #define IDC_VALUE 1003 #define IDC_READCELL 1004 #define IDC_WRITECELL 1005 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 103 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//Spreadsheet.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h " END 2 TEXTINCLUDE BEGIN "#include ""winres.h"" " "