Golang汇编快速指南

Posted on 2015-04-23 15:40:07 golang

摘要:

这篇文档是对于Go编译器套件(6g, 8g, etc.)中不常用的汇编语言的快速预览,涵盖面不是很广泛。

Go的汇编语言基于Plan 9的汇编,Plan 9网站的页面上有详细描述。如果你想编写汇编语言,你应该读这篇文档,虽然它是Plan 9相关的。这边文档总结了汇编的语法,并且描述了使用汇编语言和Go程序交互时的特殊之处。

有一点是很重要的是,Go的汇编中没有直接体现出底层的机器。有些汇编细节能直接对应到机器,但有些不是。这是因为编译器套件在常规过程中不需要汇编语言。取而代之的是,编译器产生二进制的不完整的汇编指令集,链接器会完成它。实际上,链接器做了汇编指令的选择,所以当你看到类似于MOV这样的指令,链接器的实际操作可能不是一个移动指令,也许是清除或者载入。或者可能会根据指令的名字对应到真实的机器指令。总体上,机器相关的指令操作趋向于体现出真实的机器指令,但是一些通用的概念类似于移动内存数据、调用子例程、返回等操作就更抽象了。具体的细节和架构相关,我们为这种不精确性道歉。

阅读全文

长志气戒傲气 必须时刻保持冷静

Posted on 2015-02-04 14:38:50 life

左宗棠是晚清时期著名的军事家、政治家。

1874 年,他被任命为钦差大臣,率兵赶赴西北地区平定叛乱。一天,他行走在一条乡间小路上,无意中看见路边悬挂了一面“天下第一棋手”的旗子,有位老者正悠闲地坐在下面布围棋。左宗棠素来擅长下棋,自视棋艺高超难逢对手,如今遇到这么个狂妄的老者,便信心十足地走上前,要与其一决高下。

两人各执一方开始对弈。几手下来,左宗棠感到老者棋艺不凡,招招布满了杀机,处处暗藏着险境。尽管如此,左宗棠总能够化险为夷,绝处逢生,不及百手便大败对手。老者不服气,请求再战,左宗棠照样险胜,五盘下来连战连捷。左宗棠不禁喜形于色:“你自称天下第一,其实棋艺也不过如此! ”老者羞愧难当,当即拆下旗子,灰溜溜地离去。

不久,左宗棠从前线得胜归来,重游故地时发现路边居然又挂起了“天下第一棋手”的旗子,还是那位老者在布棋。他决心再次挑战对手。两人各守一方,又开始鏖战。老者依旧招招暗藏杀机,棋路变幻莫测,可这次不论左宗棠如何竭力挣扎,奋力解围,五盘下来都是中盘告负。左宗棠不服气,约好次日再战,结果依然屡战屡败,输得心服口服。

左宗棠困惑不已:“短短数日,为何您的棋艺进步得如此神速?”老者微笑着摇了摇头:“我的棋艺与前些日子并无变化。只是上次你即将远征边关保家卫国,一旦我胜了你,必将挫伤你杀敌的锐气,所以我便送你几盘险胜的棋局,以助长你必胜的信心。而今你小胜归来,难免有些沾沾自喜,我故意一再击败你,是让你明白骄傲自满还为时尚早,必须时刻保持冷静才能最终扭转局面……”

在身负重任时要长志气,在赢得小胜时要戒傲气。左宗棠这才明白了老者一让一挫的良苦用心,感叹道:“先生不仅棋艺高明,更深谙处世之道,是我终生的老师啊! ”


python的Greenlet模块源码分析

Posted on 2015-01-26 14:58:26 c

摘要:

协程,即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程可以在运行期间的某个点上暂停执行,并在恢复运行时从暂停的点上继续执行。 协程已经被证明是一种非常有用的程序组件,不仅被python、lua、ruby等脚本语言广泛采用,而且被新一代面向多核的编程语言如golang rust-lang等采用作为并发的基本单位。 协程可以被认为是一种用户空间线程,与传统的线程相比,有2个主要的优点:

与线程不同,协程是自己主动让出CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程的使用更加清晰易懂,并且多数情况下不需要锁机制。 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常小。

协程可以认为是一种用户态的线程,与系统提供的线程不同点是,它需要主动让出CPU时间,而不是由系统进行调度,即控制权在程序员手上。既然看成是用户态线程,那必然要求程序员自己进行各个协程的调度,这样就必须提供一种机制供编写协程的人将当前协程挂起,即保存协程运行场景的一些数据,调度器在其他协程挂起时再将此协程运行场景的数据恢复,以便继续运行。这里我们将协程运行场景的数据称为上下文。

阅读全文

AT&T 汇编和 GCC 内联汇编简介

Posted on 2015-01-25 18:37:38 c

AT&T 汇编和 GCC 内联汇编简介

对一个应用程序员来讲,了解汇编不是必需的,更少有手写纯汇编的需求。但是如果能了解些基本的汇编知识,对程序调试和一些语言特性的理解是大有裨益的。本文介绍 AT&T 语法的汇编的要点以及 GCC 使用的内联汇编(inline assembly)的使用。

AT&T 汇编

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,代码寻址为 cs;off 默认为 0;index 默认为 0;scale 默认为 1。比如,有个 Message 的结构体数组,该结构体 大小为 16 字节,len 成员的偏移量为 8,数组起始地址保存在 %rbx,元素索引保存在 %rcx,那么,movq 8(rax, rcx, 16), %rdx 将数组的第 %rcx 个元素的 len 成员 load 到 %rdx 中。

通用寄存器(x86_64):

  • rax, eax, ax, ah, al;
  • rbx, ebx, bx, bh, bl;
  • rcx, ecx, cx, ch, cl;
  • rdx, edx, dx, dh, dl;
  • rsi, esi, si;
  • rdi, edi, di;
  • rbp, ebp;
  • rsp, esp;
  • r8-r15;
  • xmm0-xmm7;
  • st0-st7;
  • fs;

X86_64 下 ABI 调用约定:

  • 整型参数(包括整数、指针等),由左至右,分别使用 rdi, rsi, rdx, rcx, r8, r9 传递参数,超过 6 六个参数时,多余参数压栈传递;
  • 浮点型参数使用 xmm0-xmm7 传递,多余参数压栈传递;
  • 整型返回值使用 rax:rdx,浮点型返回值使用 xmm0:xmm1,long double 使用 st0:st1;
  • 结构体参数的传递较为复杂,可能由寄存器或者压栈传递,参考 AMD64 ABI 文档;
  • 结构体返回值,由调用方提供栈空间,并将起始地址通过 rdi 传入;
  • rbp 『一般』为栈帧(stack frame)基址;
  • rsp 为栈顶地址;
  • Linux 中使用 fs 实现 TLS(Thread Local Storage);
  • rbx, rbp, rsp, r12-r15 为 callee-saved registers,即调用其他函数不会改变此类寄存器内容。其他寄存器为 caller-saved,如有必要,函数调用方需要自行保存;

GCC 内联汇编

内联汇编允许在 C/C++ 代码中嵌入汇编代码,以优化关键代码或者使用架构特有的指令。内联汇编的基本格式如下:

asm [volatile] ( <assembler template>
    : ["constraints"(var)] [,"constraints"(var)]  /* output operands */
    : ["constraints"(var)] [,"constraints"(var)]  /* input operands */
    : ["register"] [,"register"] [,"memory"]      /* clobbered registers */
    );

中括号中为可选部分,尖括号为必选部分。圆括号内由 ‘:’ 分割为四个部分:

  • asm 为 GCC 扩展关键字,为防止和代码标识符冲突,可使用 asm 代替;
  • volatile 告诉编译器,不要试图优化圆括号中的汇编代码;
  • 『assembler template』内为指令模板,其中的操作数可以使用 %n 样式的占位符(placeholder),n 为 0-9 的数字,编译器会使用后面输入/输出部分代入。如果代码中直接使用寄存器,需要使用两个 ‘%%’, 例如 ‘%%eax’;
  • 第二和第三部分分别为输出/输入操作数说明;输入/输出部分是 C/C++ 代码和汇编代码交互的界面,用来指名汇编代码中可以使用哪些变量以及汇编代码的计算结果保存到哪些变量。变量可以为多个,以逗号分割,按照出现的顺序分别编号,汇编代码中使用该编号来引用这个变量,比如 %0 为第一个变量。每个变量的指示格式为 “contraints”(var),其中 constraints 限定了汇编代码中变量 var 可以使用的寄存器(输入变量)或者将哪个寄存器保存到变量 var 中(输出)。constraints 中可以指名多个寄存器,编译器按照实际情况任意分配其中一个。
  • 第四部分为修改说明(clobbered list)。clobbered list 中可以列举寄存器名,这些寄存器在代码中是显式使用的,而不是由编译器自动分配或者在输入/输出指名的。特殊地,“memory” 告诉编译器,汇编代码中显式使用内存地址/全局变量访问了内存,执行该段汇编之后,所有之前的寄存器需要重新加载。

常用的 constraints 为:

  • r, 分别下面子列表中寄存器的任意一个来保存 var 变量,相当于 abcdSD:
    • a, %rax, %eax, %ax, %al
    • b, %rbx, %ebx, %bx, %bl
    • c, %rcx, %ecx, %cx, %cl
    • d, %rdx, %edx, %dx, %dl
    • S, %rsi, %esi, %si
    • D, %rdi, %edi, %di
  • q, 相当于 abcd
  • m, 内存操作数
  • digit, 使用和第 #digit 个相同的寄存器
  • f, 使用一个浮点寄存器

输出 constraints 中需要下面至少一个『修饰符』(constraints modifier)作为前缀:

  • =, 此操作数仅作为输出,之前的内容可以抛弃;
  • +, 此操作数同时作为输入和输出。

下面看几个示例:

asm ("":::); //~ nothing
asm ("incl %%eax\n\t":::"eax"); //~ access register directly
asm ("movq $1, %0\n\t" : "=m"(var)); //~ write 1 to var
asm ("mov %0, %%eax\n\t" : : "m"(var)); //~ read from var to eax
//~ read a to eax, read b to either ebx|ecx|edx|edi|esi, add it to eax, write back eax to a
asm ("addl %1, %0\n\t" : "+a"(a) : "r"(b));
asm ("incq global_var\n\t" :::"memory"); //~ access global_var directly
asm ("incl %0\n\t" : "+q"(var)); //~ read var to either eax|ebx|ecx|edx, increase it, write it back to var
asm ("incl %0\n\t" : "=q"(var) : "0"(var)) //~ the same as above, constraint 0 means using the same register
asm ("incl %[__var__]\n\t" : [__var__]"+q"(var)); //~ use user-defined placeholder

最后一个示例使用了用户自定义的占位符,通常在输入输入变量较多的情况下使用,省得逐个地对应。

在汇编中调用 printf:

#include <stdio.h>
int
main()
{
  char *fmt = "Hello, %s\n";
  char *s = "World";
  int ret = 0;
  asm (" callq printf\n\t"
      : "=a"(ret)
      : "D"(fmt), "S"(s));
  printf("ret: %d\n", ret);
  return 0;
}

在汇编中进行系统调用:

int
sys_write(int fd, const char *buf, size_t n)
{
  int ret;
  asm (
      "syscall\n\t"
      : "=a"(ret)
      : "0"(1), "D"(fd), "S"(buf), "d"(n)
      );
  return ret;
}
 
int
main()
{
  char *s = "Hello, World\n";
  printf("%d\n", sys_write(fileno(stdout), s, strlen(s)));
  return 0;
}

参考资料

  • Professional Assembly Language, Richard Blum. 貌似是唯一一本以 AT&T 语法讲解汇编语言的了。
  • Programming From The Ground Up, Jonathan Bartlett, 如果上一本是以编程讲汇编的,这一本就是以汇编讲编程的了。
  • System V Application Binary Interface, AMD64 架构下的 System V ABI, 也是 Linux 使用的 ABI.

不要习惯了黑暗,就为黑暗辩护

Posted on 2015-01-14 05:23:36 life

如果天空总是黑暗的,那就摸黑生存;如果发出声音是危险的,那就保持沉默;如果自觉无力发光,那就蜷伏于墙角。 但不要习惯了黑暗就为黑暗辩护;也不要为自己的苟且而得意;不要嘲讽那些比自己更勇敢的人们。我们可以卑微如尘土,但不可扭曲如蛆虫。 –曼德拉


Golang中Timer的陷阱

Posted on 2014-10-04 14:08:31 golang

Golang的Timer类,是一个普遍意义上的定时器,它有着普通定时器的一些特性,例如:

  • 给定一个到期时间,和一个回调函数,到期后会调用回调函数
  • 重置定时器的超时时间
  • 停止定时器

Golang的Timer在源码中,实现的方式是以一个小顶堆来维护所有的Timer集合。接着启动一个独立的goroutine,循环从小顶堆中的检测最近一个到期的Timer的到期时间,接着它睡眠到最近一个定时器到期的时间。最后会执行开始时设定的回调函数。Timer到期之后,会被Golang的runtime从小项堆中删除,并等待GC回收资源。

下面给出实际的代码:

package main

import (
    "time"
    "fmt"
)


func main() {
    timer := time.NewTimer(3 * time.Second)

    go func() {
        <-timer.C
        fmt.Println("Timer has expired.")
    }()

    timer.Stop()
    time.Sleep(60 * time.Second)
}

timer.NewTimer()会启动一个新的Timer实例,并开始计时。 我们启动一个新的goroutine,来以阻塞的方式从Timer的C这个channel中,等待接收一个值,这个值是到期的时间。并打印”Timer has expired.”

到现在看起来似乎没什么问题,但是当我们执行timer.Stop()之后,3秒钟过去了,程序却没有打印那句话。说明执行timer.Stop()之后,Timer自带的channel并没有关闭,而且这个Timer已经从runtime中删除了,所以这个Timer永远不会到期。

这会导致程序逻辑错误,或者更严重的导致goroutine和内存泄露。解决的办法是,使用timer.Reset()代替timer.Stop()来停止定时器。

package main

import (
    "time"
    "fmt"
)


func main() {
    timer := time.NewTimer(3 * time.Second)

    go func() {
        <-timer.C
        fmt.Println("Timer has expired.")
    }()

    //timer.Stop()
    timer.Reset(0  * time.Second)
    time.Sleep(60 * time.Second)
}

这样做就相当于给Timer一个0秒的超时时间,让Timer立刻过期。


go程序调试总结

Posted on 2014-10-04 03:51:00 golang

摘要:

首先需要注意的是:golang1.3之后的版本,对于支持gdb调试存在很大的问题。产生这个问题的原因是,golang的runtime没有完整的被gdb支持。

最新比较完整支持gdb调试的版本是golang 1.2.2,但是也有个别问题存在。

为什么会出现以上种种问题,golang官网给出的解释是:

GDB does not understand Go programs well. The stack management, threading, and runtime contain aspects that differ enough from the execution model GDB expects that they can confuse the debugger, even when the program is compiled with gccgo. As a consequence, although GDB can be useful in some situations, it is not a reliable debugger for Go programs, particularly heavily concurrent ones. Moreover, it is not a priority for the Go project to address these issues, which are difficult. In short, the instructions below should be taken only as a guide to how to use GDB when it works, not as a guarantee of success.

翻译一下:

GDB不能很好的理解GO程序。堆栈管理,线程,而且runtime包含了非常不一样的执行模式,这不是GDB期望的,他们会扰乱调试器,即使go程序是使用gccgo编译的。结果就是,虽然GDB在某些场合下是有用的,但是对go程序来说并不是一个可靠的调试器。尤其是在大量并发的时候。而且,这不是Golang项目优先考虑的事情,这很困难。总而言之,下面的操作手册,只是当GDB正常工作的时候,引导你如何使用GDB,不能保证总是成功。

并且从google group讨论组和stackoverflow中,可以看到golang的多个版本对于GDB的支持都有这样那样的问题。 不过既然官方的手册都这么说了,我们也只有在合适的场合使用GDB吧。

默认情况下,编译过的二进制文件已经包含了 DWARFv3 调试信息,只要 GDB7.1 以上版本都可以进行调试。 在OSX下,如无法执行调试指令,可尝试用sudo方式执行gdb。

在编译go程序的时候,需要关闭内联优化:** -gcflags “-N -l”**。可以在go get/build/test的时候指定这个参数。

有两种方式可以下断点:

  • gdb命令运行之后,使用break file:lineno
  • 使用runtime.BreakPoint()
阅读全文

两张图看懂GDT、GDTR、LDT、LDTR的关系

Posted on 2014-07-26 09:24:01 os

段选择符

32位汇编中16位段寄存器(CS、DS、ES、SS、FS、GS)中不再存放段基址,而 是段描述符在段描述符表中的索引值,D3-D15位是索引值,D0-D1位是优先级(RPL)用于特权检查,D2位是描述符表引用指示位TI,TI=0指 示从全局描述表GDT中读取描述符,TI=1指示从局部描述符中LDT中读取描述符。这些信息总称段选择符(段选择子).

段描述符

8个 字节64位,每一个段都有一个对应的描述符。根据描述符描述符所描述的对象不同,描述符可分为三类:储存段描述符,系统段描述符,门描述符(控制描述 符)。在描述符中定义了段的基址,限长和访问内型等属性。其中基址给出该段的基础地址,用于形成线性地址;限长说明该段的长度,用于存储空间保护;段属性 说明该段的访问权限、该段当前在内存中的存在性,以及该段所在的特权级。

段描述符表:

IA-32处理器把所有段描述符按顺序组织成线性表 放在内存中,称为段描述符表。分为三类:全局描述符表GDT,局部描述符表LDT和中断描述符表IDT。GDT和IDT在整个系统中只有一张,而每个任务 都有自己私有的一张局部描述符表LDT,用于记录本任务中涉及的各个代码段、数据段和堆栈段以及本任务的使用的门描述符。GDT包含系统使用的代码段、数 据段、堆栈段和特殊数据段描述符,以及所有任务局部描述符表LDT的描述符。

GDTR全局描述符寄存器

48位,高32位存放GDT基址,低16为存放GDT限长。

LDTR局部描述符寄存器

16位,高13为存放LDT在GET中的索引值。

IA-32处理器仍然使用xxxx:yyyyyyyy(段选择器:偏移量)逻辑方式表示一个线性地址,那么是怎么得到段的基址呢?在上面说明中我们知道,要得到段的基址首先通过段选择符xxxx中TI位指定的段描述符所在位置: 当 TI=0时表示段描述符在GDT中,如下图所示:① 先从GDTR寄存器中获得GDT基址。② 然后再GDT中以段选择符高13位位置索引值得到段描述符。③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

当TI=1时表示段描述符在LDT中,如下图所示:① 还是先从GDTR寄存器中获得GDT基址。② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。④ 用段选择符高13位位置索引值从LDT段中得到段描述符。⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。


Golang性能调节-通过net/http/pprof

Posted on 2014-06-08 12:43:50 golang

Package pprof serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool. For more information about pprof, see http://code.google.com/p/google-perftools/.

The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/.

To use pprof, link this package into your program:

import _ "net/http/pprof"

If your application is not already running an http server, you need to start one. Add “net/http” and “log” to your imports and the following code to your main function:

go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Then use the pprof tool to look at the heap profile:

go tool pprof http://localhost:6060/debug/pprof/heap

Or to look at a 30-second CPU profile:

go tool pprof http://localhost:6060/debug/pprof/profile

Or to look at the goroutine blocking profile:

go tool pprof http://localhost:6060/debug/pprof/block

To view all available profiles, open http://localhost:6060/debug/pprof/ in your browser.

For a study of the facility in action, visit

http://blog.golang.org/2011/06/profiling-go-programs.html


理解HTTP幂等性

Posted on 2014-06-08 06:51:44 http

摘要:

基于HTTP协议的Web API是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中,我们都见到了越来越多的SOA或RESTful的Web API。为什么Web API如此流行呢?我认为很大程度上应归功于简单有效的HTTP协议。HTTP协议是一种分布式的面向资源的网络应用层协议,无论是服务器端提供Web服务,还是客户端消费Web服务都非常简单。再加上浏览器、Javascript、AJAX、JSON以及HTML5等技术和工具的发展,互联网应用架构设计表现出了从传统的PHP、JSP、ASP.NET等服务器端动态网页向Web API + RIA(富互联网应用)过渡的趋势。Web API专注于提供业务服务,RIA专注于用户界面和交互设计,从此两个领域的分工更加明晰。在这种趋势下,Web API设计将成为服务器端程序员的必修课。然而,正如简单的Java语言并不意味着高质量的Java程序,简单的HTTP协议也不意味着高质量的Web API。要想设计出高质量的Web API,还需要深入理解分布式系统及HTTP协议的特性。

阅读全文