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协议的特性。

阅读全文

自旋锁代替互斥锁的实践

Posted on 2014-05-11 02:24:20 algorithms

摘要:

自旋锁和互斥锁是多线程程序中的重要概念。 它们被用来锁住一些共享资源, 以防止并发访问这些共享数据时可能导致的数据不一致问题。 但是它们的不同之处在哪里? 我们应该在什么时候用自旋锁代替互斥锁?

阅读全文

baby

Posted on 2014-04-17 06:08:03 life

需要密码访问


Golang中操作[]byte类型的Trim函数

Posted on 2014-03-10 23:47:03 golang

摘要:

Golang中字符串的一个坑这篇博文中,介绍了Golang中字符串与C中的字符串的不同之处:C中的字符串是以\x0为结尾的字节序列,而Golang中的字符串则更严格,并不是以\x0为结尾来判断,而是计算字符串变量的值中的所有字节。

Golang中的字符串在打印时,因为\x0是不可打印字符,所以和C中的字符串在打印时没有任何区别。但是在一些需要按字节计算字符串的函数中,就会导致问题。通用的做法是,迭代字符串,只取得字节序列中的有效字节。

我们可以写一个函数来做这件事情,也可以用Golang的标准库中的bytes库。bytes库提供了对[]byte类型的操作,提供的接口和对string类型提供的接口类似。

阅读全文