God's in his heaven.
All's right with the world.

0%

基础语法

  • 由于C++的枚举不像C#中的枚举,其枚举类型名并不是标识符的一部分,因此经常可能发生命名冲突的问题,解决的方法有四个:在枚举元素名称前加限定前缀(如enum EnumFruit { EnumFruit_apple = 1 };),将枚举类型放在一个同名的命名空间中,或将枚举作为类的嵌套类型,或者使用C++11的enum class(What’s an enum class and why should I care?)。
  • struct和class的默认类继承方式都是private,这与struct的成员默认继承方式是public是不同的。
  • #include_next <filename.h>,include位于搜索路径中位于当前文件之后的文件filename.h。
  • 在vc中,inlucde的路径的反斜杠不需要转义,如#include "..\..\..\Global\Data\GlobalPreferencesMgr.h"
  • 对于namespace中的函数或class的前置声明,必须同样也包括在相同的namespace中,而不能用class ::std::A这种写法。(Why can’t I forward-declare a class in a namespace like this?
  • 没有&&=,只有&=
  • (-1 || 0) == 1,请想想为什么。
阅读全文 »

当我们在键盘上敲下一个字母的时候,到底是怎么发送到相应的进程的呢?我们通过ps、who等命令看到的类似tty1、pts/0这样的输出,它们的作用和区别是什么呢?

阅读全文 »

Problem

Transactions are widely used in database development, especially when a database supports an application where a lot of correlated changes are needed. A transaction is a group of SQL statements that are all committed together (all changes done by these statements become a permanent part of the database) or none of these statements are committed (all these changes are rolled back). More often statements included in transaction are DML (Data Manipulation Language) statements, such as INSERT, UPDATE, DELETE and so on. But what about DDL (Data Definition Language) statements? Is it possible to include DDL commands such as CREATE, ALTER, DROP, etc., in a transaction? In this tip we are going to answer these questions for both - MS SQL Server and Oracle databases.

Solution

The approaches to use DDL commands within transactions are quite different in Microsoft SQL Server vs. Oracle. Let’s discuss this for each RDBMS separately.

阅读全文 »

cache

看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

我不知道为什么这么多人用的都是这个逻辑,当我在微博上发了这个贴以后,我发现好些人给了好多非常复杂和诡异的方案,所以,我想写这篇文章说一下几个缓存更新的Design Pattern(让我们多一些套路吧)。

这里,我们先不讨论更新缓存和更新数据这两个事是一个事务的事,或是会有失败的可能,我们先假设更新数据库和更新缓存都可以成功的情况(我们先把成功的代码逻辑先写对)。

更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching,我们下面一一来看一下这四种Pattern。

阅读全文 »

因为太了解软件,我很慎重在自己的电脑上安装新软件。大约半年前,有朋友通过百度云盘向我传递dump文件。点击链接下载时失败,提示超过了普通方式允许的上限,必须安装百度云盘客户端软件。于是我的电脑新增了一个软件,名曰“百度云管家”。第一次看到这个名字,就觉得很奇怪,云的家在服务器上,为什么一个终端应用程序叫云管家呢?

过了一段时间,我慢慢意识到,原来这位云管家管的不是云的家,而是我(用户电脑)的家。名字的含义或曰:“我是百度云,来管你的家”。

为什么这么认为呢?最初的原因是我发现这个管家特别忙碌,即使当我根本没有使用百度云。更让我跌破眼镜的是,即使我把网线拔掉、关闭无线,它依然忙碌。这些反常的表现让我不得不留意它了。多少次,我打开任务管理器,看它忙碌的身影。多少次,我想大声对它说:“管家大哥,你歇歇,告诉我你在忙啥?”

怎一个忙字了得

图1 任务管理器中缺页异常排名第一

起初是在任务管理器中发现百度云管家(以下简称其“管家程序”)很忙。图1是我某次看见它忙时做的截图。在这个截图中,系统中一共运行了175个进程,任务列表是按缺页异常总数(Page Faults)排名,管家程序排名第一位,而且遥遥领先,把一向排名靠前的McAfee安全软件(第二名和第三名)远远抛在后头(相差一个数量级)。顺便说下,排在第4位的BaiduProtect是管家程序的同门兄弟,以后台服务方式运行,权限更高。

在Page Faults右侧的那一列是PF Delta,代表最近一秒钟新增的缺页异常个数,管家程序新增4千多个,但这并不是我看到的最高值,有时是7000多。再往右的一列是CPU净时间,即CPU执行管家程序的累计时间,14分37秒。这个数值也算较高了,因为系统中CPU频率高达2.6 GHz,速度很快,排在后面的很多程序(图1中未显示出)的累计时间还不到1秒。排在第二名的安全软件CPU累计时间是1小时55分42秒,比管家程序还高很多。如果把缺页异常总数除以CPU净时间,便得到缺页异常与CPU净时间的比率。这个比率反应了CPU执行程序时触发缺页异常的频繁程度,不妨将其称为缺页异常净频率。为排名前两位的两个程序计算这个指标,其结果如下:

1
2
3
4
0:000> ?? 215415833/(14*60+37)
int 0n245628
0:000> ?? 34064443/(115*60+42)
int 0n4907

可以看出管家程序触发缺页异常的净频率高得惊人,达到24万多次。这意味着CPU平均执行这个程序1秒钟就触发24万多个缺页异常。这也意味着,CPU花在这个程序上的时间有很多都用在了处理缺页异常上。

图2 使用Process Explorer观察线程信息

图2是使用Mark Russinovich先生的Process Explorer来观察管家程序的截图,显示的是管家程序的线程信息。

可以看到,管家程序有四个很活跃的线程,它们的CPU占用率都超过了0.1%。图2中第1列是线程ID,第2列是CPU占用率,第3列是Cycles Delta,即最近一秒钟CPU执行这个线程所用的时钟个数。从Windows Vista开始,NT内核会读取现代处理器的性能计数器来统计CPU花在每个线程上的时钟个数。根据图2,最近1秒里,管家程序的前4个线程使用的CPU时钟数分别为1千4百万、1千5百万、3千2百万和1亿零5百万。

图2下方是排名第一的8864号线程的更多数据,其中的Kernel和User分别是内核态净时间(23秒多)和用户态净时间(1分23秒多)。Context Switches是用户态和内核态之间切换的次数,高达3千1百多万次。左下角的Cycles是CPU执行该线程时所用的总时钟个数,7万多亿个。今天的x86处理器使用的超标量架构有4个发射端口,每次可以发出四条指令乱序执行,这意味着每个时钟周期可能执行多达四条指令。对于比较差的情况,平均每条指令所用的时钟周期(即所谓的CPI指标,Cycles Per Instruction)可能为3。按CPI为3来折算一下,CPU在这个线程上执行的指令数多达2万多亿条。2万多亿条指令是什么概念呢?曾经轰动信息产业的著名CIH病毒,总指令数只有几百条。即使按1千条来说,那么2万多亿条指令相当于把CIH病毒执行了20多亿次。

图3 VTune显示的线程信息

图3是使用Intel的著名调优工具VTune分析管家程序时得到的线程信息。每行代表一个线程。需要说明的是,因为图2与图3是针对管家程序的不同运行实例,所以无法用线程ID把两个线程对应起来。但观察到的结论是一致的,从VTune视图来看,也是有四个线程很繁忙,而且有很频繁的线程上下文切换。VTune视图给我们的另一个信息是,有多个线程的执行过程都很有规律,尤其是第四个,每隔大约1秒(横轴为时间,单位为秒)有个尖峰,这说明该线程很可能是受定时器触发来工作的。

上调试器

上面使用多个工具观察管家程序得到的结论都是它很忙碌。但不是很清楚到底是在忙什么?熟悉我的朋友一定想到了要上调试器。诚然,要想深刻认识软件,没有比调试器更有力的工具了。唤出WinDBG,附加到管家进程,一切顺利,先执行lm浏览模块信息(图4)。

图4 模块列表(部分结果)

图4中,第一行是EXE主模块,接下来的kernelbasis、kernel和kernelpromote三个模块的名字中都含有kernel字样,第一次看到这些名字让我一惊,以为与系统的kernel32和kernelbase模块有关,后来确认这是云管家自己的模块,我不禁好奇,这么高大上的名字,不知道出自哪位同行的妙想。

顺便说一下,图4中以Yun开头的YunDb和YunLogic模块也是管家程序的重要模块,后文会提到。

观察EXE模块的详细信息(图5),可以看到目前使用的是2016年3月的版本,这比我最初分析过的版本要新很多。

图5 主模块的版本信息
图6 自动更新模块

在已经卸载的模块列表(图6)中,可以看到一个名为AutoUpdateUtil.dll的模块多次出现,它应该是用来做自动更新的。

大致了解模块信息后,执行*观察线程信息。哇,一共40多个线程。执行*e .echo _*_; ? @$tid;.ttime观察线程的执行时间信息,可以看到有几个线程的CPU累计时间都超过了秒级。这与前面使用Process Explorer看到的结果一致。

反调试与反反调试

做了以上观察后,执行g命令,希望让管家程序走走看。但是意外出现了,WinDBG很快收到了进程退出事件。第一次看到这一幕时,不禁愕然。凭借多年经验,我意识到这次的对手不一般,也是懂调试的,检测到调试器后,主动退出了。“你上调试器,我不跑了,死给你看。”

管家程序的这招反调试让我刷新了对百度同行的认识。但我并没有被这招吓到,反而兴趣更高了。不禁让我想起曾经在国内某公司的一次交流,在我演讲之后,一位同行提问,“看过了你写的《软件调试》,是否有计划写一本如何反调试的?”

调试是软件世界里的逃生通道,我真的不愿意写反调试的书。
但被逼到这里,只好出几招了。首先需要知道管家程序检测调试器的方法。先退出调试器,触发管家程序重新执行,并再附加WinDBG,然后执行x kernelbase!_debug_列出Windows系统的调试API。其中的IsDebuggerPresent是用来检测是否在被调试的最简单方法,对其设置断点,而后执行g恢复管家程序执行。

刹那之间,断点果然命中,k命令观察,真的是上文曾提到的Yun字辈模块之一YunLogic在调用这个检测调试器的API(图7)。

图7 检测调试器

使用u命令观察IsDebuggerPresent函数,很短。其原理我在《软件调试》中有详细介绍,先通过TEB取得PEB,再访问PEB中的BeingDebugged字段。

1
2
3
4
5
KERNELBASE!IsDebuggerPresent:
76153789 64a118000000 mov eax,dword ptr fs:[00000018h] fs:0053:00000018=7efdd000
7615378f 8b4030 mov eax,dword ptr [eax+30h]
76153792 0fb64002 movzx eax,byte ptr [eax+2]
76153796 c3 ret

单步跟踪到ret指令,看EAX寄存器果然为1,代表调试器存在。

1
2
0:000> r eax
eax=00000001

如果把这个结果返回给管家程序,那么它就发现被调试了,继而就会开始退出。于是,执行r eax=0,“狸猫换太子”。
这样篡改IsDebuggerPresent的结果后,再g恢复执行,发现断点再次命中,看来是“骗过”管家一次,它又一次做检查。
每次修改返回值太麻烦了。执行a命令开始交互式汇编(图8)。

图8 WinDBG的交互式汇编支持

WinDBG的汇编环境虽然简陋,但也足够用了,输入以下两行x86汇编后,直接按回车键结束汇编。

1
2
Mov eax, 0
Ret

再观察IsDebuggerPresent API,现在变成了下面这样:

1
2
3
KERNELBASE!IsDebuggerPresent:
76153789 b800000000 mov eax,0
7615378e c3 ret

也就是永远返回假。这样偷梁换柱之后,先bd * 禁止断点,然后再执行g命令恢复管家执行。这下它不退出了,因为它以为调试器不在。

原来管家程序的反调试设施如此单薄。看了它的模块架构,其实有一种更简单有效的反调试方法,不过老雷不想说,因为我一向不赞成反调试。

折腾堆

解除了管家程序的反调试保护之后,可以进一步寻找它忙碌的原因了。经过一番勘察,我发现管家程序忙碌的第一个原因是非常频繁地分配和释放内存。长话短说,在WinDBG中设置如下断点来监视从堆上的内存分配。

1
bp ntdll!RtlAllocateHeap+5 ".echo **allocating heap;r $t1=@$t1+1; ? @$t1; kv;.if(poi(ebp+10)>10000){}.else{gc;}"

先解释一下上面的断点命令,地址部分加5是为了越过函数开头的序言部分,以保证后面获取到参数值是准确的。双引号中包含了多条命令,先是显示提示信息,然后使用一个准变量来统计断点命中次数并打印出来,之后的kv是显示栈回溯,而后判断第三个参数所代表的分配大小是否超过1MB,如果超过则中断,不然则gc继续执行。

设好以上断点,恢复目标执行,发现大量信息喷涌而出,如图9所示。

图9 频繁的内存分配

等待5分钟左右,没有自动中断,说明没有发生参数超过1MB的调用,手工中断下来,可以看到$t1的累计值高达6万多次。

1
2
0:043> ? @$t1
Evaluate expression: 61434 = 0000effa

如果再设置如下断点监视释放堆块的行为,那么即使过了十几分钟之后,t1的值仍然不大。

1
bp ntdll!RtlFreeHeap+0x5 ".echo **Releasing heap;r $t1=@$t1-1; ? @$t1; kv; gc"

这说明很多内存块是分配了后,很快又释放掉了。有经验的程序员知道,从堆上分配内存是开销比较大的操作,好的程序应该尽可能减少从堆上分配内存的次数,分配好了的堆快如果将来还可能使用,那么最好重复使用,不要释放了又分配,分配了又释放。

枚举进程

管家程序的更大问题是频繁调用很重的系统API。执行如下命令对系统的CreateToolhelp32Snapshot API设置断点。

1
bp kernel32!CreateToolhelp32Snapshot ".echo creating snapshot;? @$tid;r $t8=@$t8+1;? @$t8;kv;gc"

禁止其他断点后,恢复目标执行,会发现这个断点命中的也很频繁。一分钟调用了100多次,大约每秒钟调用两次,如图10所示。

图10 频繁调用CreateToolhelp32Snapshot API

熟悉Windows操作系统开发的朋友知道,CreateToolhelp32Snapshot的用途是对指定进程或者系统中的所有进程抓取快照。其函数原型为:

1
bp kernel32!CreateToolhelp32Snapshot ".echo creating snapshot;? @$tid;r $t8=@$t8+1;? @$t8;kv;gc"

参考图10中的kv命令结果,可以看到dwFlags参数为2,代表TH32CS_SNAPPROCESS,意为包含系统中的所有进程。

把上述断点中的gc去掉,不要自动恢复执行,断点命中后,一边观察任务管理器窗口,一边执行gu命令,执行完这个API后中断,可以发现每调用CreateToolhelp32Snapshot一次大约触发60多个缺页异常。

CreateToolhelp32Snapshot返回的是一个句柄,通常拿到这个句柄后再反复调用Process32Next API来获取每个进程的信息。设置如下断点:

1
2
3
4
HANDLE WINAPI CreateToolhelp32Snapshot(
_In_ DWORD dwFlags,
_In_ DWORD th32ProcessID
);

恢复管家程序执行,可以看到以上断点果然反复命中,如图11所示。

图11 反复调用Process32NextW API

根据老雷的试验观察,每调用一次Process32NextW API,大约会触发8次缺页异常。管家程序每调用好一次CreateToolhelp32Snapshot后,会调用165次Process32NextW,那么这两项导致的缺页异常总数加起来便是1300多次,即:

1
bp kernel32!Process32NextW ".echo enumerating each process;r $t9=@$t9+1;? @$t9;gc"

管家程序每秒钟会做两轮以上循环,于是便是2千多次了。值得说明的是,这个很重的循环操作发生在一个线程中,即前文所说图3中很有规律的第4个线程。有读者可能会问,如果每秒循环两次,那么图3中的尖峰应该是间隔半秒啊?其实不然,因为这个线程是连续循环两次。也就是每次唤醒后,连续做两次拍照和枚举,然后休息不到1秒再做两轮循环,如此往复。执行.ttime观察这个线程的执行时间,可以看到它的执行时间很长。

1
2
3
4
0:017> .ttime
Created: Mon May 2 09:56:18.377 2016 (UTC + 8:00)
Kernel: 0 days 0:02:45.579
User: 0 days 0:00:24.679

执行~17n命令把这个线程临时挂起,恢复管家程序,再观察任务管理器,发现PF Delta(每秒钟新增的缺页异常)指标立刻降下来了,只有不到十次了。看来导致管家程序那么多的缺页异常的主要原因在于这个枚举系统进程的线程。它忙着给系统里的所有进程拍照,然后再一个个看过来。重要的是,这样的工作不是做一次,而是每秒来两轮,风雨无阻、孜孜不倦,时时刻刻关心着系统里运行着的其它进程,好辛劳的管家啊。
软件的历史不长,但软件的孩提时代已经过去了,因为今天的软件已经丧失了曾经拥有的简单和纯真,变得复杂、贪婪和狡黠。

一年多之前,我曾写过一篇《在调试器里看阿里的软件兵团》,批评了支付宝客户端软件中的性能问题,文章发表后,很高兴看到阿里的同行不断改进,今天已经不再有当时的问题了(图1中还可以看到淘宝的TBSecSvc进程,排名已经比较靠后)。不知百度的同行看过此文有何感想?作为一款客户端软件,能帮助用户管家是好想法,但是管家毕竟是仆人,有事时应该尽心给主人办事,没事时应该安安静静休息,不要肆意挥霍主人家的东西。


本文地址:http://xnerv.wang/debugging-baidu-pan/
转载自:在调试器里看百度云管家

单例模式是一种很简单常用的设计模式,常见的做法可能是这样:

1
2
3
4
5
Renderer& getInstance()
{
static Renderer renderer;
return renderer;
}

当然,这个代码在不支持static local variable thread-safe init的编译器上,是没有办法保证线程安全的,c++11标准已经规定static local variable只会被初始化一次了,然而vs2013还没有实现,vs2015里才支持了这条标准.不过这条代码在我们的程序里只有一个线程访问,所以也就不存在线程安全的问题.

阅读全文 »

进程的用户空间

Windows系统中,系统空间与用户空间的分界线为0x80000000,该地址以上为系统空间,以下为用户空间,各占2GB(默认情况下)。2GB的用户空间不是全部可访问的,它被划分成了以下几个部分:

用户空间的两端各有64KB的隔离区,是不允许访问的。上端的隔离区将用户空间与系统空间隔离开,下端的隔离区就是我们编程时遇到的NULL,因此NULL实际所表示的区域不是一个地址,而是一个64KB的地址范围。这样,实际可访问的用户空间地址范围为0x10000-0x7ffeffff。

阅读全文 »

linux约定

经常看到linux中,共享库的名字后面跟了一串数字,比如:libperl.so.5.18.2。其实就是版本号,作用是为了更加方便的管理动态库,比如升级。往往系统中存在一个库的多个版本,那么Linux 系统如何控制多个版本的问题?Window之前没有处理好,为此专门有个名词来形容这个问题:“Dll hell”,其严重影响软件的升级和维护。“Dll hell”是指windows上动态库的新版本覆盖了旧版本,但是却不兼容老版本,所以程序升级之后,动态库更新导致程序运行不起来。在Linux操作系统下也有同样的问题,那么它是怎么解决的呢?

Linux引入了一套机制,如果遵守这个机制就可以避免这个问题。 但是这只事一个约定,不是强制的。通常建议用户遵守这个约定,否则也会出现Linux版的“Dll hell”问题。 下面来介绍一个这个机制。 这个机制是通过文件名,来控制动态库(shared library)的版本。

阅读全文 »

一 介绍

MySQL 5.6版本提供了很多性能优化的特性,其中之一就是 Multi-Range Read 多范围读(MRR) , 它的作用针对基于辅助/第二索引的查询,减少随机IO,并且将随机IO转化为顺序IO,提高查询效率。

二 原理

在没有MRR之前,或者没有开启MRR特性时,MySQL 针对基于辅助索引的查询策略是这样的:

1
select non_key_column from tb wherekey_column=x;

MySQL 执行查询的伪代码

1
2
3
4
5
6
第一步 先根据where条件中的辅助索引获取辅助索引与主键的集合,结果集为rest。
select key_column, pk_column from tb where key_column=x order by key_column

第二步 通过第一步获取的主键来获取对应的值。
for each pk_column value in rest do:
select non_key_column from tb where pk_column=val
阅读全文 »