目錄1
1.官網1
2.功能1
3.環境要求2
4.實現原理2
5.SIGHUP信號處理 3
6.重啟線程4
7.重啟目標程序5
8.系統調用鉤子輔助6
9.被勾住系統調用exit 6
10.被勾住系統調用listen 7
11.SymbolVersioning8
12.勾住bind等系統調用 10
13.系統調用過程13
14.測試代碼13
14.1.Makefile13
14.2.s.c14
14.3.s.map14
14.4.x.cpp14
14.5.體驗方法15
https://github.com/amscanne/huptime
零停重啟目標程序,比如一個網絡服務程序,不用丟失和中斷任何消息實現重新啟動,正在處理的消息也不會中斷和丟失,重啟的方法是給目標程序的進程發SIGHUP信號。
由於使用了Google牛人TomHerbert為Linux內核打的補丁SO_REUSEPORT特性,因此要求Linux內核版本為3.9或以上,SO_REUSEPORT允許多個進程監聽同一IP的同一端口。
利用SIGHUP+SO_REUSEPORT+LD_PRELOAD,通過LD_PRELOAD將自己(huptime.so)注入到目標進程空間。
使用Python腳本huptime啟動時會設置LD_PRELOAD,將huptime.so注入到目標程序的進程空間。
huptime.so啟動時會執行setup函數,在setup中會創建一個線程impl_restart_thread用於重啟目標程序的進程,另外還會安裝信號SIGHUP的處理器sighandler用於接收零重啟信號SIGHUP:
staticvoid__attribute__((constructor))
setup(void)
{
#definelikely(x)__builtin_expect(!!(x),1)
if(likely(initialized))//只做一次
return;
initialized=1;
#defineGET_LIBC_FUNCTION(_name)\
libc._name=get_libc_function<_name##_t>(#_name,&_name)
//初始化全局變量libc,讓其指向GLIBC庫的bind等
GET_LIBC_FUNCTION(bind);//libc.bind=dlsym(RTLD_NEXT,bind);//系統的bind
GET_LIBC_FUNCTION(listen);
GET_LIBC_FUNCTION(accept);
GET_LIBC_FUNCTION(accept4);
GET_LIBC_FUNCTION(close);
GET_LIBC_FUNCTION(fork);
GET_LIBC_FUNCTION(dup);
GET_LIBC_FUNCTION(dup2);
GET_LIBC_FUNCTION(dup3);
GET_LIBC_FUNCTION(exit);
GET_LIBC_FUNCTION(wait);
GET_LIBC_FUNCTION(waitpid);
GET_LIBC_FUNCTION(syscall);
GET_LIBC_FUNCTION(epoll_create);
GET_LIBC_FUNCTION(epoll_create1);
#undefGET_LIBC_FUNCTION
impl_init();//安裝信號SIGHUP處理器、創建重啟線程等
}
template
staticFUNC_T
get_libc_function(constchar*name,FUNC_Tdef)
{
char*error;
FUNC_Tresult;
/*Clearlasterror(ifany).*/
dlerror();
/*Trytogetthesymbol.*/
result=(FUNC_T)dlsym(RTLD_NEXT,name);
error=dlerror();
if(result==NULL||error!=NULL)
{
fprintf(stderr,"dlsym(RTLD_NEXT,\"%s\")failed:%s",name,error);
result=def;
}
returnresult;
}
//信號SIGHUP處理函數,作用是通過管道通知重啟線程impl_restart_thread,
//這裡其實可以考慮使用eventfd替代pipe
staticvoid*impl_restart_thread(void*);
void
sighandler(intsigno)
{
/*Notifytherestartthread.
*Wehavetodothisinaseparatethread,because
*wehavenoguaranteesaboutwhichthreadhasbeen
*interruptedinordertoexecutethissignalhandler.
*Becausethiscouldhavehappenedduringacritical
*section(i.e.locksheld)wehavenochoicebutto
*firetherestartasycnhronouslysothatittoocan
*grablocksappropriately.*/
if(restart_pipe[1]==-1)
{
/*We'vealreadyrun.*/
return;
}
while(1)
{
chargo='R';
intrc=write(restart_pipe[1],&go,1);//通知重啟線程
if(rc==0)
{
/*Wat?Tryagain.*/
continue;
}
elseif(rc==1)
{
/*Done.*/
libc.close(restart_pipe[1]);
restart_pipe[1]=-1;
break;
}
elseif(rc<0&&(errno==EAGAIN||errno==EINTR))
{
/*Goagain.*/
continue;
}
else
{
/*Shit.*/
DEBUG("Restartpipefubared!?Sorry.");
break;
}
}
}
void*
impl_restart_thread(void*arg)
{
/*Waitforoursignal.*/
while(1)
{
chargo=0;
intrc=read(restart_pipe[0],&go,1);//等待SIGHUP信號
if(rc==1)
{
/*Go.*/
break;
}
elseif(rc==0)
{
/*Wat?Restart.*/
DEBUG("Restartpipeclosed?!");
break;
}
elseif(rc<0&&(errno==EAGAIN||errno==EINTR))
{
/*Keeptrying.*/
continue;
}
else
{
/*Realerror.Let'srestart.*/
DEBUG("Restartpipefubared?!");
break;
}
}
libc.close(restart_pipe[0]);
restart_pipe[0]=-1;
/*Seenoteaboveinsighandler().*/
impl_restart();//重啟目標進程
returnarg;
}
void
impl_restart(void)
{
/*Indicatethatwearenowexiting.*/
L();//加鎖
impl_exit_start();
impl_exit_check();
U();//解鎖
}
funcs_timpl=
{
.bind=do_bind,
.listen=do_listen,
.accept=do_accept_retry,
.accept4=do_accept4_retry,
.close=do_close,
.fork=do_fork,
.dup=do_dup,
.dup2=do_dup2,
.dup3=do_dup3,
.exit=do_exit,
.wait=do_wait,
.waitpid=do_waitpid,
.syscall=(syscall_t)do_syscall,
.epoll_create=do_epoll_create,
.epoll_create1=do_epoll_create1,
};
funcs_tlibc;//目標程序的進程調用的實際是huptime中的do_XXX系列
staticvoid
do_exit(intstatus)
{
if(revive_mode==TRUE)//如果是復活模式,也就是需要重啟時
{
DEBUG("Reviving...");
impl_exec();//調用execve重新啟動目標程序
}
libc.exit(status);//調用系統的exit
}
staticint
do_listen(intsockfd,intbacklog)
{
intrval=-1;
fdinfo_t*info=NULL;
if(sockfd<0)
{
errno=EINVAL;
return-1;
}
DEBUG("do_listen(%d,...)...",sockfd);
L();
info=fd_lookup(sockfd);
if(info==NULL||info->type!=BOUND)
{
U();
DEBUG("do_listen(%d,%d)=>-1(notBOUND)",sockfd,backlog);
errno=EINVAL;
return-1;
}
/*Checkifwecanshort-circuitthis.*/
if(info->bound.real_listened)
{
info->bound.stub_listened=1;
U();
DEBUG("do_listen(%d,%d)=>0(stub)",sockfd,backlog);
return0;
}
/*Canwereallycalllisten()?*/
if(is_exiting==TRUE)
{
info->bound.stub_listened=1;
U();
DEBUG("do_listen(%d,%d)=>0(is_exiting)",sockfd,backlog);
return0;
}
/*Welargelyignorethebacklogparameter.People
*don'treallyusesensiblevalueshereforthemost
*part.Hopefully(asisdefaultonsomesystems),
*tcpsyncookiesareenabled,andthere'snoreal
*limitforthisqueueandthisparameterissilently
*ignored.Ifnot,thenweusethelargestvaluewe
*cansensiblyuse.*/
(void)backlog;
rval=libc.listen(sockfd,SOMAXCONN);
if(rval<0)
{
U();
DEBUG("do_listen(%d,%d)=>%d",sockfd,backlog,rval);
returnrval;
}
/*We'redone.*/
info->bound.real_listened=1;
info->bound.stub_listened=1;
U();
DEBUG("do_listen(%d,%d)=>%d",sockfd,backlog,rval);
returnrval;
}
Huptime使用到了GCC的基於符號的版本機制SymbolVersioning。本節內容主要源自:https://blog.blahgeek.com/glibc-and-symbol-versioning/。
在linux上運行一個在其他機器上編譯的可執行文件時可能會遇到錯誤:/lib64/libc.so.6:version‘GLIBC_2.14’notfound(requiredby./a.out),該錯誤的原因是GLIBC的版本偏低。
從GLIBC2.1開始引入了SymbolVersioning機制,每個符號對應一個版本號,一個glibc庫可包含一個函數的多個版本:
#nm/lib64/libc.so.6|grepmemcpy
000000000008ee90imemcpy@@GLIBC_2.14
00000000000892b0imemcpy@GLIBC_2.2.5
其中memcpy@@GLIBC_2.14為默認版本。使用SymbolVersioning可以改變一個已存在的接口:
__asm__(".symveroriginal_foo,foo@");
__asm__(".symverold_foo,foo@VERS_1.1");
__asm__(".symverold_foo1,foo@VERS_1.2");
__asm__(".symvernew_foo,foo@@VERS_2.0");
如果沒有指定版本號,這個示例中的“foo@”代表了符號foo。源文件應當包含四個C函數的實現:original_foo、old_foo、old_foo1和new_foo。它的MAP文件必須在VERS_1.1、VERFS_1.2和VERS_2.0中包含foo。
也可以在自己的庫中使用SymbolVersioning,如:
///@filelibx-1.c@date05/10/2015
__asm__(".symverfoo_1,foo@@libX_1.0");
intfoo_1(){
return1;
}
__asm__(".symverbar_1,bar@@libX_1.0");
intbar_1(){
return-1;
}
配套的MAP文件:
libX_1.0{
global:
foo;
bar;
local:*;
};
編譯:
gcc-shared-fPIC-Wl,--version-scriptlibx-1.maplibx-1.c-olib1/libx.so
當發布新版本,希望保持兼容,可以增加一個版本號:
///@filelibx.c@date05/10/2015
/*oldfoo*/
__asm__(".symverfoo_1,foo@libX_1.0");
intfoo_1(){
return1;
}
/*newfoo*/
__asm__(".symverfoo_2,foo@@libX_2.0");
intfoo_2(){
return2;
}
__asm__(".symverbar_1,bar@@libX_1.0");
intbar_1(){
return-1;
}
相應的MAP文件變成:
libX_1.0{
global:
foo;
bar;
local:*;
};
libX_2.0{
global:foo;
local:*;
};
設置環境變量LD_DEBUG,可以打開動態鏈接器的調試功能。共享庫的構造和析構函數:
void__attribute__((constructor(5)))init_function(void);
void__attribute__((destructor(10)))fini_function(void);
括號中的數字越小優先級越高,不可以使用gcc-nostartfiles或-nostdlib。通過鏈接腳本可以將幾個現在的共享庫通過一定方式組合產生新的庫:
GROUP(/lib/libc.so.6/lib/libm.so.2)
/*Exportsnameasaliasnamein.dynsym.*/
#definePUBLIC_ALIAS(name,aliasname)\
typeof(name)aliasname__attribute__((alias(#name)))\
__attribute__((visibility("default")));
/*Exportsstub_##nameasname@version.*/
#defineSYMBOL_VERSION(name,version,version_ident)\
PUBLIC_ALIAS(stub_##name,stub_##name##_##version_ident);\
asm(".symverstub_"#name"_"#version_ident","#name"@"version);
/*Exportsstub_##nameasname@@(i.e.,theunversionedsymbolforname).*/
#defineGLIBC_DEFAULT(name)\
SYMBOL_VERSION(name,"@",default_)
/*Exportsstub_##nameasname@@GLIBC_MAJOR.MINOR.PATCH.*/
#defineGLIBC_VERSION(name,major,minor)\
SYMBOL_VERSION(name,"GLIBC_"#major"."#minor,\
glibc_##major##minor)
#defineGLIBC_VERSION2(name,major,minor,patch)\
SYMBOL_VERSION(name,"GLIBC_"#major"."#minor"."#patch,\
glibc_##major##minor##patch)
GLIBC_DEFAULT(bind)//當目標程序調用bind時,實際調用的將是Huptime庫中的stub_bind
GLIBC_VERSION2(bind,2,2,5)
GLIBC_DEFAULT(listen)
GLIBC_VERSION2(listen,2,2,5)
GLIBC_DEFAULT(accept)
GLIBC_VERSION2(accept,2,2,5)
GLIBC_DEFAULT(accept4)
GLIBC_VERSION2(accept4,2,2,5)
GLIBC_DEFAULT(close)
GLIBC_VERSION2(close,2,2,5)
GLIBC_DEFAULT(fork)
GLIBC_VERSION2(fork,2,2,5)
GLIBC_DEFAULT(dup)
GLIBC_VERSION2(dup,2,2,5)
GLIBC_DEFAULT(dup2)
GLIBC_VERSION2(dup2,2,2,5)
GLIBC_DEFAULT(dup3)
GLIBC_VERSION2(dup3,2,2,5)
GLIBC_DEFAULT(exit)
GLIBC_VERSION(exit,2,0)
GLIBC_DEFAULT(wait)
GLIBC_VERSION2(wait,2,2,5)
GLIBC_DEFAULT(waitpid)
GLIBC_VERSION2(waitpid,2,2,5)
GLIBC_DEFAULT(syscall)
GLIBC_VERSION2(syscall,2,2,5)
GLIBC_DEFAULT(epoll_create)
GLIBC_VERSION2(epoll_create,2,3,2)
GLIBC_DEFAULT(epoll_create1)
GLIBC_VERSION(epoll_create1,2,9)
對應的MAP文件:
GLIBC_2.2.5{
global:
bind;
listen;
accept;
accept4;
close;
fork;
dup;
dup2;
dup3;
syscall;
local:*;
};
GLIBC_2.3.2{
global:
epoll_create;
local:*;
};
GLIBC_2.0{
global:
exit;
local:*;
};
GLIBC_2.9{
global:
epoll_create1;
local:*;
};
GLIBC_DEFAULT(bind)展開
typeof(stub_bind)stub_bind_default___attribute__((alias("stub_bind")))__attribute__((visibility("default")));;
asm(".symverstub_""bind""_""default_"",""bind""@""@");
//上面這一句等效於:asm(.symverstub_bind_default_,bind@@);
GLIBC_VERSION2(bind,2,2,5)
typeof(stub_bind)stub_bind_glibc_225__attribute__((alias("stub_bind")))__attribute__((visibility("default")));;
asm(".symverstub_""bind""_""glibc_225"",""bind""@""GLIBC_""2"".""2"".""5");
//上面這一句等效於:asm(.symverstub_bind_glibc_225,bind@GLIBC_2.2.5);
以bind為例:
目標程序的進程->stub_bind->impl.bind->do_bind->libc.bind
impl為一全局變量,impl->bind為函數指針,指向於do_bind。而libc.bind也為一函數指針,指向系統的bind。
用於體驗SymbolVersioning和勾住系統函數:
1)Makefile
用於編譯和測試
2)s.c
實現勾住庫函數memcpy。
3)s.map
s.c的MAP文件。
4)x.cpp
用於測試被勾住的memcpy程序。
all:x
x:x.cpplibS.so
g++-g-o$@$<
libS.so:s.cs.map
gcc-g-shared-fPIC-D_GNU_SOURCE-Wl,--version-scripts.map$<-o$@
clean:
rm-flibS.sox
test:all
exportLD_PRELOAD=`pwd`/libS.so;./x;exportLD_PRELOAD=
#include
#include
void*stub_memcpy(void*dst,constvoid*src,size_tn)
{
printf("stub_memcpy\n");
void*(*libc_memcpy)(void*,constvoid*,size_t)=dlsym(RTLD_NEXT,"memcpy");
returnlibc_memcpy(dst,src,n);
}
typeof(stub_memcpy)stub_memcpy_default___attribute__((alias("stub_memcpy")))__attribute__((visibility("default")));;
asm(".symverstub_""memcpy""_""default_"",""memcpy""@""@");
libS_1.0{
global:
memcpy;
local:*;
};
//Test:
//exportLD_PRELOAD=`pwd`/libS.so;./x;exportLD_PRELOAD=
#include
#include
intmain()
{
chardst[100]={'1','2','3','\0'};
constchar*src="abc";
memcpy(dst,src,strlen(src)+1);
printf("%s\n",dst);
return0;
}
直接執行maketest即可:
$maketest
exportLD_PRELOAD=`pwd`/libS.so;./x;exportLD_PRELOAD=
stub_memcpy
abc
如果不勾,則不要設置LD_PRELOAD直接執行x:
$./x
abc