Clamav杀毒软件源码分析笔记[十]
2008-10-13 21:13
477 查看
Clamav杀毒软件源码分析笔记[十]
刺猬@http://blog.csdn.net/littlehedgehog
[客户端处理]
服务端已经把主要的工作都已经处理的差不多了,剩下来也就是服务端等待客户端提出请求,然后根据客户端的请求做相应的工作. 所以客户端所做的事情就只是提交数据,然后坐享其成.服务端是在垂帘听政呢,还是做幕后英雄?话不多说了,下面进入正题.
客户端(clamdscan)的主函数main和clamscan其实用的是同一个main,当然里面的内容同样还是处理命令行,配置文件等相关信息,乏善可陈,经过这系列处理之后,我们来到了client这个客户端的逻辑处理主函数.但是我还是不准备详细来说这个函数,因为它除了因为客户端只是创建socket和使用connect对服务端进行连接而不需要啥绑定,监听,实在和服务端的创建没多少代码上的区别. 只是在连接的时候,我们要分配置文件里是设定的本地连接还是网络连接. 如下所示:
/* 创建套间字 并建立链接*/
int dconnect(const struct optstruct *opt)
{
struct sockaddr_un server; //这里定义了两个地址,注意一个为AF_UNIX,而另一个为AF_INET
struct sockaddr_in server2;
struct hostent *he;
struct cfgstruct *copt, *cpt;
const char *clamav_conf = getargl(opt, "config-file"); //从config-file中获取配置文件名
int sockd;
if (!clamav_conf)
clamav_conf = DEFAULT_CFG;
if ((copt = parsecfg(clamav_conf, 1)) == NULL)
{
mprintf("@Can't parse the configuration file./n");
return -1;
}
memset((char *) &server, 0, sizeof(server));
memset((char *) &server2, 0, sizeof(server2));
/* Set default address to connect to 这里默认是设置为本地地址*/
server2.sin_addr.s_addr = inet_addr("127.0.0.1");
if (cfgopt(copt, "TCPSocket") && cfgopt(copt, "LocalSocket")) //TCPSocket 和 LocalSocket 只能配置一个
{
mprintf("@Clamd is not configured properly./n");
return -1;
}
else if ((cpt = cfgopt(copt, "LocalSocket"))) //本地socket,也就是用AF_UNIX地址域啦
{
server.sun_family = AF_UNIX;
strncpy(server.sun_path, cpt->strarg, sizeof(server.sun_path));
if ((sockd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
mprintf("@Can't create the socket./n");
return -1;
}
if (connect(sockd, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0)
{
close(sockd);
perror("connect()");
mprintf("@Can't connect to clamd./n");
return -1;
}
}
else if ((cpt = cfgopt(copt, "TCPSocket")))
{
if ((sockd = socket(SOCKET_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
mprintf("@Can't create the socket./n");
return -1;
}
server2.sin_family = AF_INET;
server2.sin_port = htons(cpt->numarg);
if ((cpt = cfgopt(copt, "TCPAddr")))
{
if ((he = gethostbyname(cpt->strarg)) == 0) //这里默认的地址其实是127.0.0.1 还是本地的
{
close(sockd);
perror("gethostbyname()");
mprintf("@Can't lookup clamd hostname./n");
return -1;
}
server2.sin_addr = *(struct in_addr *) he->h_addr_list[0]; //确实可能存在一个主机对应多个IP地址,比如多个网卡,这里只取第一个了
}
if (connect(sockd, (struct sockaddr *) &server2, sizeof(struct sockaddr_in)) < 0)
{
close(sockd);
perror("connect()");
mprintf("@Can't connect to clamd./n");
return -1;
}
}
else
{
mprintf("@Clamd is not configured properly./n");
return -1;
}
return sockd;
}
建立完连接之后,我们要做得就是提交各种任务给服务端让它做了,不能让它手下一大批线程给闲着.这里我们还是依照书上内容,看看stream扫描:
else if (!strcmp(opt->filename, "-")) /* scan data from stdin 这个是作者的规定,哈哈我也没办法 比如cat testfile | clamscan - 后面加一个"-"表明从stdin获得数据流 所以这里用了cat的输出作为扫描的输入 */
{
if ((sockd = dconnect(opt)) < 0)
return 2;
if ((ret = dsstream(sockd, opt)) >= 0)
*infected += ret;
else
errors++;
close(sockd);
}
stream的扫描就是在命令行里面加上一个'-', 利用linux强大的重定向功能,我们把文件的内容作为数据流传给服务端.
/* 把文件标准输入流通过临时套间字传递给服务端扫描
* 整个过程如下:
* 1 通知服务端我们要传数据流过来
* 2 服务端分配临时端口,这个端口是为了接收我们的数据流设定的
* 3 传数据流过去呗
*/
int dsstream(int sockd, const struct optstruct *opt)
{
int wsockd, loopw = 60, bread, port, infected = 0;
struct sockaddr_in server;
struct sockaddr_in peer;
int peer_size;
char buff[4096], *pt;
//这里告诉服务端我们传的是数据流
if (write(sockd, "STREAM", 6) <= 0)
{
mprintf("@Can't write to the socket./n");
return 2;
}
memset(buff, 0, sizeof(buff));
/* 读取服务端传过来的端口号 */
while (loopw)
{
read(sockd, buff, sizeof(buff));
if ((pt = strstr(buff, "PORT")))
{
pt += 5;
sscanf(pt, "%d", &port); //注意sscanf用法
break;
}
loopw--;
}
if (!loopw)
{
mprintf("@Daemon not ready for stream scanning./n");
return -1;
}
/* connect to clamd */
if ((wsockd = socket(SOCKET_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
mprintf("@Can't create the socket./n");
return -1;
}
server.sin_family = AF_INET;
server.sin_port = htons(port);
/* 通过socket获取对方信息*/
peer_size = sizeof(peer);
if (getpeername(sockd, (struct sockaddr *) &peer, &peer_size) < 0)
{
perror("getpeername()");
mprintf("@Can't get socket peer name./n");
return -1;
}
/*看看对方在哪里,是本地还是外面的...*/
switch (peer.sin_family)
{
case AF_UNIX:
server.sin_addr.s_addr = inet_addr("127.0.0.1");
break;
case AF_INET:
server.sin_addr.s_addr = peer.sin_addr.s_addr;
break;
default:
mprintf("@Unexpected socket type: %d./n", peer.sin_family);
return -1;
}
if (connect(wsockd, (struct sockaddr *) &server, sizeof(struct sockaddr_in)) < 0)
{
close(wsockd);
perror("connect()");
mprintf("@Can't connect to clamd [port: %d]./n", port);
return -1;
}
/* 从标准输入读取数据流然后写入到socket 服务端自会接应*/
while ((bread = read(0, buff, sizeof(buff))) > 0)
{
if (write(wsockd, buff, bread) <= 0)
{
mprintf("@Can't write to the socket./n");
close(wsockd);
return -1;
}
}
close(wsockd);
memset(buff, 0, sizeof(buff));
/* 下面是服务端的反馈消息 */
while ((bread = read(sockd, buff, sizeof(buff))) > 0)
{
mprintf("%s", buff);
if (strstr(buff, "FOUND/n"))
{
infected++;
logg("%s", buff);
}
else if (strstr(buff, "ERROR/n"))
{
logg("%s", buff);
return -1;
}
memset(buff, 0, sizeof(buff));
}
return infected;
}
这里有个小问题 在42行有句 if ((wsockd = socket(SOCKET_INET, SOCK_STREAM, 0)) < 0)我们直接定死了是SOCKET_INET,那如果服务端在本地怎么办呢? 看看67行的处理: server.sin_addr.s_addr = inet_addr("127.0.0.1"); 也就是如果是UNIX域就是用本地IP地址作为sockaddr的地址了.
如果我们扫描到了病毒,这里我们就需要转移病毒文件了,因为这只是个扫毒软件,不能杀毒:
/* 隔离病毒文件 说的比较通俗点儿就是转移目录,转移了...*/
void move_infected(const char *filename, const struct optstruct *opt)
{
char *movedir, *movefilename, *tmp, numext[4 + 1];
struct stat fstat, mfstat;
int n, len, movefilename_size;
struct utimbuf ubuf;
if (!(movedir = getargl(opt, "move"))) //文件隔离的目录
{
/* Should never reach here */
mprintf("@getargc() returned NULL/n", filename);
notmoved++;
return;
}
/* 检查调用进程是否可以对指定的文件执行某种操作 */
if (access(movedir, W_OK|X_OK) == -1)
{
mprintf("@error moving file '%s': cannot write to '%s': %s/n", filename, movedir, strerror(errno));
notmoved++;
return;
}
/* 查看病毒文件的stat */
if (stat(filename, &fstat) == -1)
{
mprintf("@Can't stat file %s/n", filename);
mprintf("Try to run clamdscan with clamd privileges/n");
notmoved++;
return;
}
if (!(tmp = strrchr(filename, '/')))
tmp = (char *) filename;
/* numext 是指如果病毒文件在隔离目录下面有同名文件,那我们要加后缀,比如有个virus文件,那就在后面加上一些字符串,避免同一文件夹下同名文件冲突,这个就是后缀预留空间 */
movefilename_size = sizeof(char) * (strlen(movedir) + strlen(tmp) + sizeof(numext) + 2);
if (!(movefilename = mmalloc(movefilename_size)))
{
mprintf("@Memory allocation error/n");
exit(2);
}
if (!(strrcpy(movefilename, movedir)))
{
mprintf("@strrcpy() returned NULL/n");
notmoved++;
free(movefilename);
return;
}
/* "路径名/" */
strcat(movefilename, "/");
/* "路径名/文件名" */
if (!(strcat(movefilename, tmp)))
{
mprintf("@strcat() returned NULL/n");
notmoved++;
free(movefilename);
return;
}
/* 这里我们用完整的路径名试探,看看这个文件存在不,呃,就这样吧,不知道怎么表达了 */
if (!stat(movefilename, &mfstat))
{
if (fstat.st_ino == mfstat.st_ino) /* It's the same file 通过inode号检验确实是同一个文件,注意文件名相同不能判定同一文件 */
{
mprintf("File excluded '%s'/n", filename);
logg("File excluded '%s'/n", filename);
notmoved++;
free(movefilename);
return;
}
else //到这里了,那就是隔离目录下面有同名文件
{
/* file exists - try to append an ordinal number to the
* quranatined file in an attempt not to overwrite existing
* files in quarantine
*/
len = strlen(movefilename);
n = 0;
do
{
/* reset the movefilename to it's initial value by
* truncating to the original filename length
*/
movefilename[len] = 0;
/* append .XXX */
sprintf(numext, ".%03d", n++); //右边对齐 最后格式是: 病毒名.032
strcat(movefilename, numext); //加后缀
}
while (!stat(movefilename, &mfstat) && (n < 1000)); //这里只试探1000次,我想应该够了吧,除非你PC成了毒窝了...
}
}
/* 两种方式来处理 第一个就是改名字,其实文件都在磁盘,具体路径还不是我们自己设定而已,所以改名就能转移目录*/
if (rename(filename, movefilename) == -1)
{
if (filecopy(filename, movefilename) == -1) //第二种方法就是直接拷贝了...
{
mprintf("@cannot move '%s' to '%s': %s/n", filename, movefilename, strerror(errno));
notmoved++;
free(movefilename);
return;
}
/* 下面由于我们是新建一个文件,然后拷贝原文件内容,所以文件权限和时间属性都变了,这里要改回来*/
chmod(movefilename, fstat.st_mode);
chown(movefilename, fstat.st_uid, fstat.st_gid);
ubuf.actime = fstat.st_atime;
ubuf.modtime = fstat.st_mtime;
utime(movefilename, &ubuf);
if (unlink(filename)) //删除原文件
{
mprintf("@cannot unlink '%s': %s/n", filename, strerror(errno));
notremoved++;
free(movefilename);
return;
}
}
mprintf("%s: moved to '%s'/n", filename, movefilename);
logg("%s: moved to '%s'/n", filename, movefilename);
free(movefilename);
}
转移主要就是一个copy,再加上同名文件的处理问题,有时候当你电脑成病毒窝的时候,可能病毒很多,统统转移到一个目录下面的时候造成同名冲突的可能性比较打,为了避免覆盖,这里就加上后缀. 这个好像很普遍的处理办法吧....
刺猬@http://blog.csdn.net/littlehedgehog
[客户端处理]
服务端已经把主要的工作都已经处理的差不多了,剩下来也就是服务端等待客户端提出请求,然后根据客户端的请求做相应的工作. 所以客户端所做的事情就只是提交数据,然后坐享其成.服务端是在垂帘听政呢,还是做幕后英雄?话不多说了,下面进入正题.
客户端(clamdscan)的主函数main和clamscan其实用的是同一个main,当然里面的内容同样还是处理命令行,配置文件等相关信息,乏善可陈,经过这系列处理之后,我们来到了client这个客户端的逻辑处理主函数.但是我还是不准备详细来说这个函数,因为它除了因为客户端只是创建socket和使用connect对服务端进行连接而不需要啥绑定,监听,实在和服务端的创建没多少代码上的区别. 只是在连接的时候,我们要分配置文件里是设定的本地连接还是网络连接. 如下所示:
/* 创建套间字 并建立链接*/
int dconnect(const struct optstruct *opt)
{
struct sockaddr_un server; //这里定义了两个地址,注意一个为AF_UNIX,而另一个为AF_INET
struct sockaddr_in server2;
struct hostent *he;
struct cfgstruct *copt, *cpt;
const char *clamav_conf = getargl(opt, "config-file"); //从config-file中获取配置文件名
int sockd;
if (!clamav_conf)
clamav_conf = DEFAULT_CFG;
if ((copt = parsecfg(clamav_conf, 1)) == NULL)
{
mprintf("@Can't parse the configuration file./n");
return -1;
}
memset((char *) &server, 0, sizeof(server));
memset((char *) &server2, 0, sizeof(server2));
/* Set default address to connect to 这里默认是设置为本地地址*/
server2.sin_addr.s_addr = inet_addr("127.0.0.1");
if (cfgopt(copt, "TCPSocket") && cfgopt(copt, "LocalSocket")) //TCPSocket 和 LocalSocket 只能配置一个
{
mprintf("@Clamd is not configured properly./n");
return -1;
}
else if ((cpt = cfgopt(copt, "LocalSocket"))) //本地socket,也就是用AF_UNIX地址域啦
{
server.sun_family = AF_UNIX;
strncpy(server.sun_path, cpt->strarg, sizeof(server.sun_path));
if ((sockd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
mprintf("@Can't create the socket./n");
return -1;
}
if (connect(sockd, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0)
{
close(sockd);
perror("connect()");
mprintf("@Can't connect to clamd./n");
return -1;
}
}
else if ((cpt = cfgopt(copt, "TCPSocket")))
{
if ((sockd = socket(SOCKET_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
mprintf("@Can't create the socket./n");
return -1;
}
server2.sin_family = AF_INET;
server2.sin_port = htons(cpt->numarg);
if ((cpt = cfgopt(copt, "TCPAddr")))
{
if ((he = gethostbyname(cpt->strarg)) == 0) //这里默认的地址其实是127.0.0.1 还是本地的
{
close(sockd);
perror("gethostbyname()");
mprintf("@Can't lookup clamd hostname./n");
return -1;
}
server2.sin_addr = *(struct in_addr *) he->h_addr_list[0]; //确实可能存在一个主机对应多个IP地址,比如多个网卡,这里只取第一个了
}
if (connect(sockd, (struct sockaddr *) &server2, sizeof(struct sockaddr_in)) < 0)
{
close(sockd);
perror("connect()");
mprintf("@Can't connect to clamd./n");
return -1;
}
}
else
{
mprintf("@Clamd is not configured properly./n");
return -1;
}
return sockd;
}
建立完连接之后,我们要做得就是提交各种任务给服务端让它做了,不能让它手下一大批线程给闲着.这里我们还是依照书上内容,看看stream扫描:
else if (!strcmp(opt->filename, "-")) /* scan data from stdin 这个是作者的规定,哈哈我也没办法 比如cat testfile | clamscan - 后面加一个"-"表明从stdin获得数据流 所以这里用了cat的输出作为扫描的输入 */
{
if ((sockd = dconnect(opt)) < 0)
return 2;
if ((ret = dsstream(sockd, opt)) >= 0)
*infected += ret;
else
errors++;
close(sockd);
}
stream的扫描就是在命令行里面加上一个'-', 利用linux强大的重定向功能,我们把文件的内容作为数据流传给服务端.
/* 把文件标准输入流通过临时套间字传递给服务端扫描
* 整个过程如下:
* 1 通知服务端我们要传数据流过来
* 2 服务端分配临时端口,这个端口是为了接收我们的数据流设定的
* 3 传数据流过去呗
*/
int dsstream(int sockd, const struct optstruct *opt)
{
int wsockd, loopw = 60, bread, port, infected = 0;
struct sockaddr_in server;
struct sockaddr_in peer;
int peer_size;
char buff[4096], *pt;
//这里告诉服务端我们传的是数据流
if (write(sockd, "STREAM", 6) <= 0)
{
mprintf("@Can't write to the socket./n");
return 2;
}
memset(buff, 0, sizeof(buff));
/* 读取服务端传过来的端口号 */
while (loopw)
{
read(sockd, buff, sizeof(buff));
if ((pt = strstr(buff, "PORT")))
{
pt += 5;
sscanf(pt, "%d", &port); //注意sscanf用法
break;
}
loopw--;
}
if (!loopw)
{
mprintf("@Daemon not ready for stream scanning./n");
return -1;
}
/* connect to clamd */
if ((wsockd = socket(SOCKET_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket()");
mprintf("@Can't create the socket./n");
return -1;
}
server.sin_family = AF_INET;
server.sin_port = htons(port);
/* 通过socket获取对方信息*/
peer_size = sizeof(peer);
if (getpeername(sockd, (struct sockaddr *) &peer, &peer_size) < 0)
{
perror("getpeername()");
mprintf("@Can't get socket peer name./n");
return -1;
}
/*看看对方在哪里,是本地还是外面的...*/
switch (peer.sin_family)
{
case AF_UNIX:
server.sin_addr.s_addr = inet_addr("127.0.0.1");
break;
case AF_INET:
server.sin_addr.s_addr = peer.sin_addr.s_addr;
break;
default:
mprintf("@Unexpected socket type: %d./n", peer.sin_family);
return -1;
}
if (connect(wsockd, (struct sockaddr *) &server, sizeof(struct sockaddr_in)) < 0)
{
close(wsockd);
perror("connect()");
mprintf("@Can't connect to clamd [port: %d]./n", port);
return -1;
}
/* 从标准输入读取数据流然后写入到socket 服务端自会接应*/
while ((bread = read(0, buff, sizeof(buff))) > 0)
{
if (write(wsockd, buff, bread) <= 0)
{
mprintf("@Can't write to the socket./n");
close(wsockd);
return -1;
}
}
close(wsockd);
memset(buff, 0, sizeof(buff));
/* 下面是服务端的反馈消息 */
while ((bread = read(sockd, buff, sizeof(buff))) > 0)
{
mprintf("%s", buff);
if (strstr(buff, "FOUND/n"))
{
infected++;
logg("%s", buff);
}
else if (strstr(buff, "ERROR/n"))
{
logg("%s", buff);
return -1;
}
memset(buff, 0, sizeof(buff));
}
return infected;
}
这里有个小问题 在42行有句 if ((wsockd = socket(SOCKET_INET, SOCK_STREAM, 0)) < 0)我们直接定死了是SOCKET_INET,那如果服务端在本地怎么办呢? 看看67行的处理: server.sin_addr.s_addr = inet_addr("127.0.0.1"); 也就是如果是UNIX域就是用本地IP地址作为sockaddr的地址了.
如果我们扫描到了病毒,这里我们就需要转移病毒文件了,因为这只是个扫毒软件,不能杀毒:
/* 隔离病毒文件 说的比较通俗点儿就是转移目录,转移了...*/
void move_infected(const char *filename, const struct optstruct *opt)
{
char *movedir, *movefilename, *tmp, numext[4 + 1];
struct stat fstat, mfstat;
int n, len, movefilename_size;
struct utimbuf ubuf;
if (!(movedir = getargl(opt, "move"))) //文件隔离的目录
{
/* Should never reach here */
mprintf("@getargc() returned NULL/n", filename);
notmoved++;
return;
}
/* 检查调用进程是否可以对指定的文件执行某种操作 */
if (access(movedir, W_OK|X_OK) == -1)
{
mprintf("@error moving file '%s': cannot write to '%s': %s/n", filename, movedir, strerror(errno));
notmoved++;
return;
}
/* 查看病毒文件的stat */
if (stat(filename, &fstat) == -1)
{
mprintf("@Can't stat file %s/n", filename);
mprintf("Try to run clamdscan with clamd privileges/n");
notmoved++;
return;
}
if (!(tmp = strrchr(filename, '/')))
tmp = (char *) filename;
/* numext 是指如果病毒文件在隔离目录下面有同名文件,那我们要加后缀,比如有个virus文件,那就在后面加上一些字符串,避免同一文件夹下同名文件冲突,这个就是后缀预留空间 */
movefilename_size = sizeof(char) * (strlen(movedir) + strlen(tmp) + sizeof(numext) + 2);
if (!(movefilename = mmalloc(movefilename_size)))
{
mprintf("@Memory allocation error/n");
exit(2);
}
if (!(strrcpy(movefilename, movedir)))
{
mprintf("@strrcpy() returned NULL/n");
notmoved++;
free(movefilename);
return;
}
/* "路径名/" */
strcat(movefilename, "/");
/* "路径名/文件名" */
if (!(strcat(movefilename, tmp)))
{
mprintf("@strcat() returned NULL/n");
notmoved++;
free(movefilename);
return;
}
/* 这里我们用完整的路径名试探,看看这个文件存在不,呃,就这样吧,不知道怎么表达了 */
if (!stat(movefilename, &mfstat))
{
if (fstat.st_ino == mfstat.st_ino) /* It's the same file 通过inode号检验确实是同一个文件,注意文件名相同不能判定同一文件 */
{
mprintf("File excluded '%s'/n", filename);
logg("File excluded '%s'/n", filename);
notmoved++;
free(movefilename);
return;
}
else //到这里了,那就是隔离目录下面有同名文件
{
/* file exists - try to append an ordinal number to the
* quranatined file in an attempt not to overwrite existing
* files in quarantine
*/
len = strlen(movefilename);
n = 0;
do
{
/* reset the movefilename to it's initial value by
* truncating to the original filename length
*/
movefilename[len] = 0;
/* append .XXX */
sprintf(numext, ".%03d", n++); //右边对齐 最后格式是: 病毒名.032
strcat(movefilename, numext); //加后缀
}
while (!stat(movefilename, &mfstat) && (n < 1000)); //这里只试探1000次,我想应该够了吧,除非你PC成了毒窝了...
}
}
/* 两种方式来处理 第一个就是改名字,其实文件都在磁盘,具体路径还不是我们自己设定而已,所以改名就能转移目录*/
if (rename(filename, movefilename) == -1)
{
if (filecopy(filename, movefilename) == -1) //第二种方法就是直接拷贝了...
{
mprintf("@cannot move '%s' to '%s': %s/n", filename, movefilename, strerror(errno));
notmoved++;
free(movefilename);
return;
}
/* 下面由于我们是新建一个文件,然后拷贝原文件内容,所以文件权限和时间属性都变了,这里要改回来*/
chmod(movefilename, fstat.st_mode);
chown(movefilename, fstat.st_uid, fstat.st_gid);
ubuf.actime = fstat.st_atime;
ubuf.modtime = fstat.st_mtime;
utime(movefilename, &ubuf);
if (unlink(filename)) //删除原文件
{
mprintf("@cannot unlink '%s': %s/n", filename, strerror(errno));
notremoved++;
free(movefilename);
return;
}
}
mprintf("%s: moved to '%s'/n", filename, movefilename);
logg("%s: moved to '%s'/n", filename, movefilename);
free(movefilename);
}
转移主要就是一个copy,再加上同名文件的处理问题,有时候当你电脑成病毒窝的时候,可能病毒很多,统统转移到一个目录下面的时候造成同名冲突的可能性比较打,为了避免覆盖,这里就加上后缀. 这个好像很普遍的处理办法吧....
相关文章推荐
- clamav杀毒软件源码分析笔记[一]
- Clamav杀毒软件源码分析笔记[四]
- Clamav杀毒软件源码分析笔记[五]
- Clamav杀毒软件源码分析笔记[二]
- Clamav杀毒软件源码分析笔记[七]
- Clamav杀毒软件源码分析笔记[八]
- Clamav杀毒软件源码分析笔记[三]
- Clamav杀毒软件源码分析笔记[六]
- Clamav杀毒软件源码分析笔记[九]
- Clamav杀毒软件源码分析
- zeromq源码分析笔记之线程间收发命令(2)
- 学习笔记 《鸟哥的私房菜——软件安装:源码和Tarball》
- 使用VS TFS源码分析软件PATFS进行数据附件大小限制的自定义设置
- 我的内核学习笔记10:Intel GPIO驱动源码分析
- jQuery的Deferred对象测试笔记以及源码分析
- Java笔记---ArrayList源码分析
- 笔记——软件开发这点事系列课程(2):软件需求分析
- Centos6.8系统安装代码杀毒软件:ClamAV