Select 使用不當引發的core,你應該知道的
排查一個死機問題,搞了好幾天時間,最終確定原因;最終確定問題原因,在此分享一下;
第一步:常規根據core文件查看棧信息,gdb –c core xxxx
如下rip不正確,指令地址錯亂,棧信息已破壞;在此基礎上準確定位非常困難,但是仍可發現一些線索;
根據當前棧信息,大概尋找到懷疑的函數
查看整個棧上下信息,看有無懷疑的函數:
所以很有可能就是fetchNSAddrEv函數導致,需要重點關註;
更深入的細節,限於匯編不深入,比較難分析,不過可以有另外途徑;
第二步:因為core是能復現出來,所以思路是重新編譯版本,增加編譯選項-fstack-protector
-fstack-protector:
啟用堆棧保護,不過只為局部變量中含有 char 數組的函數插入保護代碼。
-fstack-protector-all:
啟用堆棧保護,為所有函數插入保護代碼。
詳細參見:http://www.cnblogs.com/napoleon_liu/archive/2011/02/14/1953983.html
復現後,棧結構如下:
這個棧結構,看著比較爽了; AsyncConnect返回時stackcheck檢查棧溢出了;
第三步、審查代碼,所以重點排查該函數:
先貼下代碼
int CHttpHandler::AsyncConnect(const char * pszIpAddr, unsigned short usPort, float fSelectTimeout, const std::string& host) { if (strlen(pszIpAddr) > 16) { SetErrMsg("invalid ip format, pszIpAddr=%s", pszIpAddr); return -1; } m_nSockFD= socket(AF_INET, SOCK_STREAM, 0); m_fSelectTimeout= fSelectTimeout; if (m_nSockFD < 0) { SetErrMsg("init socket error, m_nSockFD=%d", m_nSockFD); return -2; } // set nonblock int flags = fcntl(m_nSockFD, F_GETFL, 0); if (fcntl(m_nSockFD, F_SETFL, flags | O_NONBLOCK) < 0) { close(m_nSockFD); m_nSockFD = -1; SetErrMsg("set sock nonblock error"); return -3; } // set server ip, port struct sockaddr_in serv_addr; socklen_t addr_len; bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; inet_aton(pszIpAddr, &serv_addr.sin_addr); serv_addr.sin_port = htons(usPort); addr_len = sizeof(serv_addr); //memset(m_szIPAddr, 0, 17); memset(m_szIPAddr,0,sizeof(m_szIPAddr)); if(!host.empty()) { if(host.length()>sizeof(m_szIPAddr)-1) { SetErrMsg("hostName too long ,hostName=%s,hostLen=%d,max_len=%d\n",host.c_str(),host.length(),sizeof(m_szIPAddr)); return -9; } memcpy(m_szIPAddr, host.c_str(), host.length()); } else { memcpy(m_szIPAddr, pszIpAddr, strlen(pszIpAddr)); } /*linger m_sLinger; m_sLinger.l_onoff = 1; m_sLinger.l_linger = 0; setsockopt(m_nSockFD, SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));*/ // connect int nRetCode; nRetCode = connect(m_nSockFD, (struct sockaddr *)&serv_addr, addr_len); if (nRetCode < 0) { if (errno != EINPROGRESS) { SetErrMsg("connect error, errno=%d", errno); close(m_nSockFD); m_nSockFD = -1; return -4; } } if (nRetCode == 0) { ; } else { //warning oss find select cause crash while select timeout fd_set rset, wset; FD_ZERO(&rset); FD_SET(m_nSockFD, &rset); wset = rset; struct timeval tv; tv.tv_sec = (int)m_fSelectTimeout; long usec = int ((m_fSelectTimeout - (int)m_fSelectTimeout)*1000000 ); tv.tv_usec = usec; nRetCode = select(m_nSockFD + 1, NULL, &wset, NULL, &tv); if (nRetCode == 0) { // timeout SetErrMsg("connect error: select timeout, select retcode=%d ", nRetCode); close(m_nSockFD); m_nSockFD = -1; return -5; } if ( FD_ISSET(m_nSockFD, &wset))//FD_ISSET(m_nSockFD, &rset) || { int error; socklen_t len = sizeof(error); if (getsockopt(m_nSockFD, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { SetErrMsg("connect error: getsockopt"); close(m_nSockFD); m_nSockFD = -1; return -6; } if (error) { SetErrMsg("connect error in select operation, error=%d", error); close(m_nSockFD); m_nSockFD = -1; return -7; } } else { SetErrMsg("connect error"); close(m_nSockFD); m_nSockFD = -1; return -8; } }
幾個人審查代碼,看了好久,沒有發現該函數有什麽問題;
第四步:檢查日誌及系統配置
進步查看日誌發現有大量的select超時的打印,而且core掉時,必然在超時事件之後,此時懷疑select調用是否會出問題;因此,首先修改select為epoll調用進行測試,問題不能復現了;
五、如此,加深懷疑slecect相關處理
@福巴找到內核代碼查看select相關實現;
當n值超過1024上限就會導致設置到數組之外,篡改掉內存;
FD_SET(m_nSockFD, &rset); 在m_nSockFD超過1024時,會導致rset數組越界,篡改後續內存;這也佐證了為什麽select很多超時錯誤,因為m_nSockFD大小越界,沒有落在select監聽的套接字集合內;
從OSS環境上看,壓測時,有大量連接並發處理,所以在壓測時才最終發現該問題;
六、總結:
所有使用select系統調用的代碼應提高警惕,系統使用的套接字超過默認配置的(1024,看系統配置)會導致這個潛在問題;
Select 使用不當引發的core,你應該知道的