比特币源码解析(13)

来源:互联网 发布:申请阿里云邮箱 编辑:程序博客网 时间:2024/05/29 14:27

0x01 AppInitBasicSetup

本章我们继续分析AppInit()中的下一个函数AppInitBasicSetup(),从这个函数开始,源码将变量初始化的过程分了很多个Step,我们也按照这样的顺序来依次分析。

Step 1: Setup

#ifdef _MSC_VER  // 如果是VS环境那么就执行    // Turn off Microsoft heap dump noise    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);    _CrtSetReportFile(_CRT_WARN, CreateFileA("NUL", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, 0));    // Disable confusing "helpful" text message on abort, Ctrl-C    _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);#endif

首先的几行代码是针对微软的VS开发环境而设置的,其中_CrtSetReportMode作用是设置消息的处理方式,该函数的详细介绍参考https://msdn.microsoft.com/zh-cn/library/1y71x448.aspx, 主要包含如下两个参数,

int _CrtSetReportMode(      int reportType,   // 报告的消息类型   int reportMode   // 处理的模式);  

对于第一个参数消息类型包括以下三种,

报表类型 描述 _CRT_WARN 警告、 消息和不需要立即关注的信息。 _CRT_ERROR 错误、 不可恢复的问题和需要立即关注的问题。 _CRT_ASSERT 断言失败 (断言表达式的计算结果为FALSE)。

针对这三种消息类型第二个参数可以指定不同的处理方式,

报告模式 _CrtDbgReport 行为 _CRTDBG_MODE_DEBUG 将消息写入调试器的输出窗口。 _CRTDBG_MODE_FILE 将消息写入用户提供的文件句柄。 _CrtSetReportFile应调用以定义要用作目标流的特定文件。 _CRTDBG_MODE_WNDW 创建一个消息框,以显示该消息以及AbortRetry,和Ignore按钮。 _CRTDBG_REPORT_MODE 返回reportMode指定reportType:1 _CRTDBG_MODE_FILE2 _CRTDBG_MODE_DEBUG4 _CRTDBG_MODE_WNDW

对于不同的消息类型我们可以同时指定不同的处理方式,例如对_CRT_WARN指定输出到终端,对_CRT_ERROR输出到文件;而如果指定的处理方式为写入文件即_CRTDBG_MODE_FILE那么还需要调用_CrtSetReportFile来设定输出的文件句柄,这也是代码的下一句,但是源码却将文件的路径设为NUL也就是空文件,说明对于警告消息不做任何处理。

接下来一个函数_set_abort_behavior是制定当程序异常终止时要采取的操作,也就是程序崩溃的时候如何处理。

转载: http://www.cppblog.com/woaidongmao/archive/2009/10/21/99129.html?opt=admin

很多软件通过设置自己的异常捕获函数,捕获未处理的异常,生成报告或者日志(例如生成mini-dump文件),达到Release版本下追踪Bug的目的。但是,到了VS2005(即VC8),Microsoft对CRT(C运行时库)的一些与安全相关的代码做了些改动,典型的,例如增加了对缓冲溢出的检查。新CRT版本在出现错误时强制把异常抛给默认的调试器(如果没有配置的话,默认是Dr.Watson),而不再通知应用程序设置的异常捕获函数,这种行为主要在以下三种情况出现。

(1) 调用abort函数,并且设置了_CALL_REPORTFAULT选项(这个选项在Release版本是默认设置的)。

(2) 启用了运行时安全检查选项,并且在软件运行时检查出安全性错误,例如出现缓存溢出。(安全检查选项 /GS 默认也是打开的)

(3) 遇到_invalid_parameter错误,而应用程序又没有主动调用_set_invalid_parameter_handler设置错误捕获函数。

所以结论是,使用VS2005(VC8)编译的程序,许多错误都不能在SetUnhandledExceptionFilter捕获到。这是CRT相对于前面版本的一个比较大的改变,但是很遗憾,Microsoft却没有在相应的文档明确指出。

虽然也可以通过_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT)signal(SIGABRT, ...)_set_invalid_parameter_handler(...)解决(1)(3),但是对于(2),设置api hook是唯一的方式。

通过上述文章我们知道,源码中_set_abort_behavior其实就是新版本的VS无法捕获到程序异常abort时的异常,通过这个函数来使得用户可以捕获到从而进行处理。

#ifdef WIN32    // Enable Data Execution Prevention (DEP)    // Minimum supported OS versions: WinXP SP3, WinVista >= SP1, Win Server 2008    // A failure is non-critical and needs no further attention!#ifndef PROCESS_DEP_ENABLE    // We define this here, because GCCs winbase.h limits this to _WIN32_WINNT >= 0x0601 (Windows 7),    // which is not correct. Can be removed, when GCCs winbase.h is fixed!#define PROCESS_DEP_ENABLE 0x00000001#endif    typedef BOOL (WINAPI *PSETPROCDEPPOL)(DWORD);    PSETPROCDEPPOL setProcDEPPol = (PSETPROCDEPPOL)GetProcAddress(GetModuleHandleA("Kernel32.dll"), "SetProcessDEPPolicy");    if (setProcDEPPol != nullptr) setProcDEPPol(PROCESS_DEP_ENABLE);#endif

这段代码是针对winows 32位系统的系统中的DEP(Data Execution Prevention,数据执行保护),是WinXP SP3,WinVista >= SP1, Win Server 2008中加入为防止缓冲区溢出攻击的一种措施,保护特定内存中的数据不能当成代码一样执行。这里之所以启用的原因是在GCC中的winbase.h中只有当系统版本满足_WIN32_WINNT >= 0x0601(Win 7)时才会启用DEP,这就导致低版本就默认没有启用DEP。启用的方法是首先从动态链接库Kernel32.dll中寻找SetProcessDEPPolicy的函数地址,该函数的介绍如下,

BOOL WINAPI SetProcessDEPPolicy(  _In_ DWORD dwFlags);

dwFlags 是DWORD类型,可以取以下几种值,

Value Meaning 0 如果DEP系统策略是OptIn或者OptOut并且DEP已经启用,那么将dwFlags设置为0,表示该进程禁用DEP。 PROCESS_DEP_ENABLE 0x00000001 为该进程永久启用DEP,通过设置PROCESS_DEP_ENABLE启用DEP之后在该进程生命周期内无法再禁用DEP。 PROCESS_DEP_DISABLE_ATL_THUNK_EMULATION 0x00000002 Disables DEP-ATL thunk emulation for the current process, which prevents the system from intercepting NX faults that originate from the Active Template Library (ATL) thunk layer. For more information, see the Remarks section. This flag can be specified only with PROCESS_DEP

源码中通过定义#define PROCESS_DEP_ENABLE 0x00000001然后传入SetProcessDEPPolicy中来启用为进程启用DEP。

接下来通过SetupNetworking()来初始化套接字,该函数的实现位于src/init.cpp line 863

// src/init.cpp line 863bool SetupNetworking(){#ifdef WIN32    // Initialize Windows Sockets    WSADATA wsadata;    int ret = WSAStartup(MAKEWORD(2,2), &wsadata);    if (ret != NO_ERROR || LOBYTE(wsadata.wVersion ) != 2 || HIBYTE(wsadata.wVersion) != 2)        return false;#endif    return true;}

在Windows下编写过网络通信程序的同学相信都知道,Windows使用Socket之前都需要先进行初始化,而这个SetupNetworking()就是起的这个作用。如果不是Windows环境那么就直接返回true

#ifndef WIN32    if (!gArgs.GetBoolArg("-sysperms", false)) {        umask(077);    }    // Clean shutdown on SIGTERM    registerSignalHandler(SIGTERM, HandleSIGTERM); // 终止信号    registerSignalHandler(SIGINT, HandleSIGTERM); // 中断信号    // Reopen debug.log on SIGHUP    registerSignalHandler(SIGHUP, HandleSIGHUP); //挂起信号    // Ignore SIGPIPE, otherwise it will bring the daemon down if the client closes unexpectedly    signal(SIGPIPE, SIG_IGN);#endif

接下来这段代码是在非Windows环境下执行的,首先来看看帮助信息对-sysperms的解释,

Create new files with system default permissions, instead of umask 077 (only effective with disabled wallet functionality)。

以系统的默认权限创建新文件,而不是077,此功能只有在禁用钱包功能的情况下才有效。

如果设置了-sysperms那么就以系统默认权限创建新的文件,如果没有设置该参数,那么通过umask来设置创建新文件时的权限,umask(077)表示创建的文件具有读写权限,创建的目录ower具有所有权限,所属组和其他用户没有任何权限。

然后三行代码分别注册了终止、中断以及挂起信号的处理函数,而这两个函数做的内容就是将相应的变量设置为true

// src/init.cpp line 287static void HandleSIGTERM(int){    fRequestShutdown = true;}static void HandleSIGHUP(int){    fReopenDebugLog = true;}

fRequestShutdown在Step 7中我们会发现是一个循环的控制变量while (!fLoaded && !fRequestShutdown) // init.cpp line 1389,当变量为true时才终止循环,这也是SIGTERM信号要做的事。

接下来的signal(SIGPIPE, SIG_IGN); 表示忽略管道断开信号,首先介绍一下signal函数,

参考: http://www.cplusplus.com/reference/csignal/signal/

signal函数是用来设置对应信号的处理函数,包括两个参数(in sig, void(*func)(int))。

第一个参数sig表示要处理的信号,有以下一些取值(参考:http://blog.csdn.net/ta893115871/article/details/7475095),

Signal Description SIGABRT 由调用abort函数产生,进程非正常退出 SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时 SIGBUS 某种特定的硬件异常,通常由内存访问引起 SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用 SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略 SIGCONT 当被stop的进程恢复运行的时候,自动发送 SIGEMT 和实现相关的硬件异常 SIGFPE 数学相关的异常,如被0除,浮点溢出,等等 SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送 SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送 SIGILL 非法指令异常 SIGINFO BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程 SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程 SIGIO 异步IO事件 SIGIOT 实现相关的硬件异常,一般对应SIGABRT SIGKILL 无法处理和忽略。中止某个进程 SIGLWP 由Solaris Thread Libray内部使用 SIGPIPE 在reader中止之后写Pipe的时候发送 SIGPOLL 当某个事件发送给Pollable Device的时候发送 SIGPROF Setitimer指定的Profiling Interval Timer所产生 SIGPWR 和系统相关。和UPS相关。 SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程 SIGSEGV 非法内存访问 SIGSTKFLT Linux专用,数学协处理器的栈异常 SIGSTOP 中止进程。无法处理和忽略。 SIGSYS 非法系统调用 SIGTERM 请求中止进程,kill命令缺省发送 SIGTHAW Solaris专用,从Suspend恢复时候发送 SIGTRAP 实现相关的硬件异常。一般是调试异常 SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程 SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送 SIGTTOU 当Background Group的进程尝试写Terminal的时候发送 SIGURG 当out-of-band data接收的时候可能发送 SIGUSR1 用户自定义signal 1 SIGUSR2 用户自定义signal 2 SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候 SIGWAITING Solaris Thread Library内部实现专用 SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程 SIGXCPU 当CPU时间限制超时的时候 SIGXFSZ 进程超过文件大小限制 SIGXRES Solaris专用,进程超过资源限制的时候发送

第二个参数表示处理的函数,取值有以下三种,

SIG_DFL 默认处理,交给系统默认处理方式处理 SIG_IGN 忽略该信号 Function handler 自定义函数处理

再来详细介绍一下SIGPIPE信号,

转载:http://www.cppblog.com/elva/archive/2008/09/10/61544.html

“当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
​ 根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。若不想客户端退出可以把SIGPIPE设为SIG_IGN。”

回到我们的源码,现在明白了这里是防止client通过socket连接到daemon之后client断开导致daemon终止的问题,而忽略SIGPIPE这个信号就可以解决。

接下来看最后一句代码,

    std::set_new_handler(new_handler_terminate);

参考:http://www.cnblogs.com/lidan/archive/2012/02/18/2357698.html

set_new_handler()函数的功能是设置当operator new无法满足某一内存分配需求时首先调用的处理函数,而源码中对new_handle_terminate的定义是

[[noreturn]] static void new_handler_terminate(){    // Rather than throwing std::bad-alloc if allocation fails, terminate    // immediately to (try to) avoid chain corruption.    // Since LogPrintf may itself allocate memory, set the handler directly    // to terminate first.    std::set_new_handler(std::terminate);    LogPrintf("Error: Out of memory. Terminating.\n");    // The log was successful, terminate now.    std::terminate();};

我们发现处理方法是直接终止进程,从而避免可能的数据冲突。