Archive for ‘边走编程’ Category

November 25, 2013

  AT&T 汇编是 GCC 所采用的语法,要点:

  • 寄存器名以 ‘%’ 为前缀:%eax
  • 立即数以 ‘$’ 为前缀:$0x80
  • 指令格式 instrunction src, dest,分别为指令名,源操作数,目的操作数,例如 mov $0, %rax
  • 操作数的宽度以指令名后缀指名,或者由操作数宽度隐式推出:单字节 b,双字节 w,四子节 l,八字节 q。例如 movb $0, (rax)
  • 相对寻址/寄存器寻址/索引寻址均由 seg:off(base, index, scale) 标识。seg 为段寄存器;off 为偏移量;base 为基址寄存器;index 为索引寄存器;scale 为索引的便宜粒度。seg/off/index/scale 均可省略:seg 默认由操作数的属性决定,数据寻址为 ds,代码寻址为 csoff 默认为 0index 默认为 0scale 默认为 1。比如,有个 Message 的结构体数组,该结构体 大小为 16 字节,len 成员的偏移量为 8,数组起始地址保存在 %rbx,元素索引保存在 %rcx,那么,movq 8(rax, rcx, 16), %rdx 将数组的第 %rcx 个元素的 len 成员 load%rdx 中。
Tags: . 1,822 views
November 24, 2013

  上文书介绍了 C++11 中的右值引用及其应用:移动语义和完美转发。本文介绍另外一个应用广泛的特性,变参模板。变参模板允许模板使用个数可变的参数类型来声明模板,包括类模板和函数模板。

Tags: . 1,157 views
November 23, 2013

  C++11 引入的新特性中,除了并发内存模型和相关设施,这些高帅富之外,最引人入胜且接地气的特性就要属『右值引用』了(rvalue reference)。加入右值引用的动机在于效率:减少不必要的资源拷贝。考虑下面的程序:

1
2
std::vector<string> v;
v.push_back("string");

  向 vector 中添加一个元素,这个动作需要先后调用 string::string(const char*), string::string(const string&), string::~string() 三个函数,涉及两次内存拷贝:第一次使用字面常量 “string” 构造出一个临时对象,第二次使用该临时对象构造出 vector 中的一个新元素,『最后临时对象会发生析构』。

Tags: . 3,702 views
November 4, 2013

  又是一个 Segmentation faltcore 在一个赋值操作, req->r->opacket = resp; 按照惯例,req, req->r 或者 req->r->opacket 指向的地址应该是非法的,但查看这些地址,却全都是合法的地址。查看汇编代码,程序 core 在指令 mov %rax, 0x40(%rdx) 处,%rdx 内容为 NULL,即 req->rNULL%rdx 的值是从 %r13 + 0x20 处取得的,而该处的值是 0x2aacf3986da0,不是 NULL
  只有一种可能:最初从 (%r13+0x20),即 req->r 取出的值(到 %rdx)是 NULL,在访问 0x40(%rdx) 之前,req->r 又被复制为非 NULL。那就是并发问题了。
  类似这种诡异的现象,可能还会遇到 assert(var != 0) 失败,但 var 却是非 0 的情况。
  遇到难以置信的 bug,就想想并发。

Tags: ,. 576 views
October 26, 2013

  说到内存屏障(Memory Barrier),就不得不提内存模型(Memory Model),但内存模型的话题太大太复杂,我仍处在初级的探索阶段,没有能力做过多的展开。笼统地讲,内存模型主要是针对多处理器环境之上的内存访问顺序、处理器间内存状态修改的可见性做了说明和限制。每个处理器架构都有自己的内存模型,属于硬件级别的内存模型。另外,很多语言(Java 5+C++11Go)在不同硬件内存模型的基础上抽象出了一致的内存模型,属于软件级别的内存模型。总之,内存模型是一个相当抽象的概念,一般来讲,只有在多处理环境做 lock-free 编程的时候才需要考虑到。理解抽象的东西,最好的方法就是通过大量接地气的具体例子,眼见为实,反复把玩,获得感性的认识,在此基础上联系抽象概念,这样才能建立系统的认知框架。

Tags: ,. 4,156 views
October 20, 2013

  上一篇介绍了 TCMallocSysAllocator,处于最底层,该层负责直接和内核交互,申请和释放内存。紧邻 SysAllocator 的上一层,是 PageHeap,负责管理内存页面。
  TCMalloc 中的页面和 Linux 内核中的页面相似,事实上,组织方式也和内核接近。介绍 PageHeap 之前,先要介绍 Span,一个 Span 是一个或多个 Page,也是 PageHeap 管理的单元:

Tags: . 507 views
October 19, 2013

  TCMalloc 的全局分配器,处于 TCMalloc 的最底层,负责向操作系统申请和释放内存,接口有两个,定义在 src/system-alloc.h|.cc

1
2
3
extern void* TCMalloc_SystemAlloc(size_t bytes, size_t *actual_bytes,
                                  size_t alignment = 0);
extern bool TCMalloc_SystemRelease(void* start, size_t length);

  TCMalloc_SystemAlloc 的参数,除了要申请的大小 bytes,还有 actual_bytesalignment,因为对齐的需求,该接口可能分配大于 bytes 的内存,实际大小保存在 actual_bytesTCMalloc_SystemRelease 负责『释放』内存,至于释放为什么加引号,下面会提到。接下来,介绍下这两个接口的实现。

Tags: . 949 views
September 23, 2013

  is_alive 实现了一个超时时间可控的 connect。如果你现在已经明白怎么回事了,请务必留下您的大名,我可是花了两周零五分钟才看出来的。
  对,就是万恶的 select,FD_SET 可能越界,因为 fd_set 中只有 FD_SETSIZE 个 bit 来标识 fd。FD_SETSIZE 是一个宏,定义在 /usr/include/sys/select.h。查看该文件,FD_SET 并未对 fd 做参数检查,因此当 fd 大于 1024 时,FD_SET 就写了它不该写的地方了,写到谁,有没有影响,有多大影响,什么时候触发,程序会不会 core 掉,什么时候 core 掉,这些都要看造化了。比如这次是写到栈里的 return address 了,导致程序跑飞。另外一个 core dump 里面,某个类的 this 从 0x1e41500 变成了 0x11e41500,当时定位不出写越界,只好认为世界末日前宇宙射线爆发导致硬件异常,聊以自慰。
  吐个槽,Linux Kernel 就不能增加一个带超时的 connect 调用?glibc 里面,FD_SETSIZE 只用在 fd_set 里的位域定义和 FD_ZERO,FD_SET 没有做任何检查;Kernel 里面的 select 实现,申请内核态 fd_set 的时候,完全依据 fd 的大小,并没有大小的限制(如果我没有漏掉某些逻辑的话)。
  总结:
  如果出现 stack corruption,几乎一定是栈的缓冲区写越界了;基本不可能是用户态的堆内存越界,因为除了主线程,每个线程的栈空间两侧又有一个 page 的虚拟空间做 gap,这些 gap,不可读,不可写,不可执行。主线程的 stack 是按需向下生长的,在 IA64 环境下,也不可能和 mmap 区域无缝相邻。当然,跳着写堆内存,或者偏移量算错就另说了。
  这篇文章中 FD_SET 写越界只篡改了一个 bit,而且被篡改的那个 bit 可能本来就是 1,非常难重现,而且 stack 的破坏程度较轻,通过查看栈内容,可以大致知道函数的调用关系。如果是类似上文 foo() 中的 memset 越界,栈可能真的面目全非了,但这时候栈内容通常是有规律可循的。
  根据栈内容判断函数调用关系,需要知道 call 指令的语义,栈中保存的是 caller 中 call 指令的下一条指令,这条指令的前一条才是 callee。

Tags: ,. 1,193 views
June 21, 2013

  分析上述汇编代码。首先获取 guard 变量,判断低字节是否为 0,若非零,表示已经初始化,可以直接使用。否则,将 guard 作为参数调用 __cxa_guard_acquire,如果锁成功,调用 init() 初始化静态变量 foo()::n,然后释放锁。如果锁失败,说明产生竞态条件,则会阻塞当前线程,不同于普通锁的地方在于,__cxa_guard_acquire 是有返回值的(当然 pthread_lock 也有返回值,但用途不同),如果发生了等待,__cxa_guard_acquire 返回 0,并不会进入 foo()::n 的初始化过程(其他线程已经初始化过了,初始化失败的情况就不细究了)。
  为了验证上述分析,可以将 init() 实现成一个耗时的操作,令多个线程“同时”调用 foo(),然后查看各个线程的运行状态。
  对于单线程程序,静态变量的保护是没有必要的,g++ 的 -fno-threadsafe-statics 选项可以禁掉该机制。

Tags: ,. 1,072 views
March 21, 2013

  多线程程序中,默认情况下各个线程的名字与进程名相同,但是可以通过操作系统提供的接口(prctl)修改这个名字。方法很简单,但某些时候,可以给程序的调试和运维带来很大的帮助。
  最近在调优tair的proxy server,其中使用到了tair client,每个tair client会创建若干个线程用于网络IO,另外proxy server本身还有其他IO线程。压测过程中发现,某个线程CPU彪到了100%,其他线程只有50%左右。根据该线程的调用栈可以推测出是tair client的IO线程,但其他IO线程不好分辨属于tair client还是proxy server。将IO线程按照功能命名后,发现4个tair client IO线程中,一个线程跑满了所在CPU核心,其他三个一直处于空闲状态。因此可以推断出tair client在连接分配上存在问题……找到了问题根源,解决起来就很容易了。

Tags: . 1,449 views
Page 1 of 1812345678910...Last »