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: .

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

Tags: .

  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: .

  又是一个 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: ,.

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

Tags: ,.

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

Tags: .

  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: .

  场景:一个多线程服务器,每个线程执行一个事件循环。在事件循环开始前,调用 socket/bind/listen 监听端口,然后将监听句柄(fd)添加到 epoll,然后开始事件循环,执行 epoll_waitepoll_wait 返回有效事件时,对于监听事件,调用 accept 建立新连接,将该连接句柄添加到 epoll;对于普通连接,调用 read/write 进行网络 IO 及其他处理逻辑。
  现象:服务器进程 CPU 占用彪高,几乎每个事件循环都在 accept,客户端出现超时。
  原因:ulimit -n65535, 进程打开的 fd 已经超过该数值,导致 accept 时无法取得 fd 而失败,而此时 TCP 连接的三次握手已经建立。又因为 epoll 使用的 LT 触发模式,该连接事件会不停地由 epoll 上报,于是产生了所谓的『死循环』,其实是事件循环闲不下来了,即使没有实际的 网络 IO。

Tags: ,.

  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: ,.
  • set follow-fork-mode child,被调试进程执行 fork 时,自动 attach;
  • set scheduler-locking on,调试时,禁用线程切换,可选 on/off/step,默认 off;
  • symbol-file target.debug,添加独立的 debuginfo 文件;
  • i sharedlibrary,查看共享文件映射信息;
  • add-symbol-file libxx.debug ADDRESS,添加共享文件的 debuginfo 文件,ADDRESS 是共享文件的映射始址,由 i sharedlibrary 获得;
  • gcc test.cpp -g -g3,调试信息中保留 MACRO;
  • set logging on,GDB 的所有输入/输出都会被写入当前目录下的 gdb.txt;
  • set print pretty on,打印对象,尤其是结构体时,格式更加友好;
  • p $rip,打印 rip 寄存器;
  • i reg,查看寄存器集,i registers-all 显示全部寄存器;
  • display/i $rip,每次断点,打印下一条指令;
  • l *0x608048,显示某指令地址对应的代码行,可执行文件包含调试信息时,亦可用 addr2line;
  • x/40a $rsp,以地址形式打印 stack,栈乱掉时可救命;
  • return 0,停止调试当前函数,并以指定值返回;
  • p {tair::StorageManager}0x608048,将指定地址以某类型打印;
  • p *array@10,打印数组 array 的前十个元素;
  • gcore,将被调试进程 core dump,gcore 还是一个独立的命令,随 GDB 发布。
Tags: .
Page 1 of 3312345678910...2030...Last »