LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 9083|回复: 8

[转]Linux下的汇编程序设计

[复制链接]
发表于 2004-5-20 09:05:35 | 显示全部楼层 |阅读模式
http://www.redflag-linux.com/source/Documents/linuxdebug.html
http://www.redflag-linux.com/source/Documents/linuxdebug.html

周天阳 1999.12

关键字: 汇编 Linux Nasm Gas

摘要:本文主要讲述了Linux下使用汇编的利弊,以及常用汇编工具的使用和语法特点。重点讲述了NASM。


引言:

汇编语言是低级语言,与硬件和操作系统紧密联系。个人电脑以前都是用DOS,现在发展成了WINDOWS98,而另一个操作系统Linux也正在崛起。下面比较一下这三个操作系统:
[php]
/ ================================================================================ \
| 操 作 系 统 |            优 点           |         缺 点                | 价 格  |
|==================================================================================|
|     DOS     | 较稳定,速度快             | 无法充分发挥计算机性能,没有 |  低    |
|             |                            | 图形界面                     |        |
|----------------------------------------------------------------------------------|
|  WINDOWS 98 | 操作简便,应用软件         | 不稳定,经常死机             |  高    |
|             | 多,硬件兼容性好           |                              |        |
|----------------------------------------------------------------------------------|
|    Linux    | 性能优秀,非常稳定,界     | 缺乏软件厂商支持,应用软件少 |  免费  |
|             | 面美观,操作简便           |                              |        |
\ -------------------------------------------------------------------------------- /
[/php]

                              表一  操作系统比较


由以上的比较可知,Linux操作系统本身具有较大优势,它的普及应该只是时间问题,所以如何在Linux下开发软件是我们计算机系学生必须学习与研究的一个课题。

Linux下的主要编程语言是C,同时Linux还支持其他许多编程语言,汇编语言作为最重要的编程语言之一,当然也包括在内。它能够完成许多其他语言所不能完成的功能。要学习Linux编程,就必须要学习Linux下的汇编程序设计。下面我就来介绍一下Linux下的汇编程序设计。


Linux汇编简介:


一、汇编语言的优缺点:

由于Linux是用C写的,所以C自然而然的就成为了Linux的标准编程语言。大部分人都把汇编给忽略了,甚至在因特网上找资料都是非常的困难,很多问题都需要靠自己来尝试。我认为这样对待汇编语言是不公平的,不能只看到它的缺点,当然也不能只看到它的优点,下面把它的优缺点作一个比较:

优点:
(1)汇编语言可以表达非常底层的东西
(2)可以直接存取寄存器和I/O
(3)编写的代码可以非常精确的被执行
(4)可以编写出比一般编译系统高效的代码
(5)可以作为不同语言或不同标准的接口

缺点:
(1)汇编语言是一个非常低级的语言
(2)非常冗长单调,在DOS下编程时就可以体会到
(3)易出BUG,且调试困难
(4)代码不易维护
(5)兼容性不好,与硬件关系非常紧密

总的来说,汇编语言要用在必须的地方,尽量少用汇编编写大型程序,多采用inline模式。


二、汇编语言工具:

DOS下常用的工具MASM和TASM到Linux下就用不起来了,Linux有自己的汇编工具,而且种类非常的多。其中gas可以算是标准配置,每一种Linux中都包括有gas,但是gas采用的不是我们通常在DOS下采用的汇编语法,它采用的是AT&T的语法格式,与intel语法格式有很大的不同。

如果要采用与DOS接近的语法格式,就必须用另一种汇编工具NASM,NASM基本与MASM相同,但也有不少地方有较大区别,特别涉及到操作系统原理时,与DOS可以说是截然不同。

Linux汇编程序设计:

1、Hello,world!

几乎所有的语言入门篇都是以“Hello,world!”为例,那么我也以Hello,world!为例开始。
[php]
; -------- NASM's standalone Hello-World.asm for Linux --------

section .text
extern puts
global main

main:
  push dword msg  ;stash the location of msg on the stack.
  call puts  ;call the 'puts' routine (libc?)
  add esp, byte 4  ;clean the stack?
  ret  ;exit.

msg:
  db "Hello World!",0
[/php]
编译:
nasm -f elf hello.asm
gcc -o hello hello.o

./hello

说明:这个程序实际上是调用了,Linux系统的puts函数,原理与调用DOS下C语言的函数相同,先用Extern声明puts是外部函数,再把参数(即msg的地址)压入堆栈,最后Call函数实现输出。

我们再来看一个程序:
[php]
section .text
global main

main:
  mov eax,4  ;4号调用
  mov ebx,1  ;ebx送1表示stdout
  mov ecx,msg  ;字符串的首地址送入ecx
  mov edx,14  ;字符串的长度送入edx
  int 80h  ;输出字串
  mov eax,1  ;1号调用
  int 80h  ;结束

msg:
  db "Hello World!",0ah,0dh
[/php]
(编译同上一个程序)

这个程序与DOS程序十分相似,它用的是linux中的80h中断,相当于DOS下的21h中断,只是因为Linux是32位操作系统,所以采用了EAX、EBX等寄存器。但是Linux作为一个多用户的操作系统与DOS又是有着非常大的区别的。要写出有特色的程序,不了解操作系统和硬件是不行的。下面我介绍一下Linux操作系统。

2、Linux操作系统简介:

操作系统实际是抽象资源操作到具体硬件操作细节之间的接口。对Linux这样的多用户操作系统来说,它需要避免用户对硬件的直接访问,并防止用户之间的互相干扰。所以Linux接管了BIOS调用和端口输入输出,关于端口输入输出方面请参阅Linux IO-Port-Programming HOWTO。而要通过Linux对硬件硬件进行访问就需要用到SystemCall,实际上是许多C的函数,可以在汇编程序中调用,调用方法与DOS下的汇编完全相同,而且用ASM汇编时不用链接额外的库函数。

Linux与DOS的主要区别在于内存管理、进程(DOS下无进程概念)、文件系统,其中内存管理和进程与汇编编程的关系比较密切:

(1)内存管理:

对任一台计算机而言,其内存以及其他资源都是有限的。为了让有限的物理内存满足应用程序对内存的大需求量,Linux采用了称为“虚拟内存”的内存管理方式。Linux将内存划分为容易处理的“内存页”,在系统运行过程中,应用程序对内存的需求大于物理内存时,Linux可将暂时不用的内存页交换到硬盘上。这样,空闲的内存页可以满足应用程序的内存需求,而应用程序却不会注意到内存交换的发生。

(2)进程

进程实际是某特定应用程序的一个运行实体。在Linux系统中,能够同时运行多个进程,Linux通过在短的时间间隔内轮流运行这些进程而实现“多任务”。这一短的时间间隔称为“时间片”,让进程轮流运行的方法称为“调度”,完成调度的程序称为调度程序。通过多任务机制,每个迸程可认为只有自己独占计算机,从而简化程序的编写,每个进程有自己单独的地址空间,并且只能由这一进程访问,这样,操作系统避免了进程之间的互相干扰以及“坏”程序对系统可能造成的危害。

为了完成某特定任务,有时需要综合两个程序的功能,例如一个程序输出文本,而另一个程序对文本进行排序。为此,操作系统还提供进程间的通讯机制来帮助完成这样的任务。Linux中常见的进程间通讯机制有信号、管道、共享内存、信号量和套接字等。

3、Linux下的汇编工具:

Linux下的汇编工具可谓百家争鸣,不像DOS下都要给MASM和TASM给控制了。但是Linux下每一种汇编工具都有很大的区别,要想全部掌握几乎是不可能的,下面我介绍几种常用的汇编工具,重点介绍NASM及其使用和语法。

(1)GCC

GCC其实是GNU的C语言产品,但它支持Inline Assemble,在GCC中inline assemble使用就像宏一样,但它比宏能更清楚更准确的表达机器的工作状态。

C是汇编编程的一个高度概括,它可以减少许多汇编中的麻烦,特别是在GCC这个C编译器中,assemble似乎起不了多大的作用。

(2)GAS

GAS是Linux各版本中基本的汇编工具,但它采用的是AT&T的语法标准与Intel的语法标准有很大的不同,对于DOS编程的我们来说,学习起来是非常困难的。当然如果要精通Linux下的汇编编程,学习GAS也是非常必要的,具体的语法标准可以参看Using GNU Assembler。

(3)GASP

GASP是GAS的扩展,它增强了GAS对宏的支持。

(4)NASM

NASM是linux中语法与DOS最为相像的一种汇编工具。虽说如此,它与MASM也是有着很大区别的。

(a)NASM的使用格式如下:

nasm -f <format> <filename> -o <filename>

例如:

Nasm -f elf hello.asm

将把hello.asm汇编成ELF object文件,而

nasm -f bin hello.asm -o hello.com

会把hello.asm汇编成二进制可执行文件hello.com

nasm -h

将会列出NASM命令行的完整说明。

NASM不会有任何输出,除非有错误发生。

-f 在Linux下主要有aout和ELF两种,如果你不确定你的Linux系统应该用AOUT还是ELF,可以在NASM目录中输入 file nasm ,如果输出nasm: ELF 32-bit LSB executable i386 (386 and up) Version 1表示是ELF,如果输出nasm: Linux/i386 demand-paged executable (QMAGIC)表示是aout。

(b)NASM与MASM的主要不同:

首先与linux系统一样,nasm是区分大小写的,Hello与hello将是不同的标识符,如果要汇编到DOS或OS/2,需要加入UPPERCASE参数。

其次,nasm中内存操作数都是以[ ]表示。

在MASM中

foo equ 1
bar dw 2
mov ax,foo
mov ax,bar

将被汇编成完全不同的指令,虽然它们在MASM中的表达方式完全一样。而NASM完全避免了这种混乱,它使用的是这样的规则:所有对内存的操作都必须通过[ ]来实现。例如上例中对bar的操作就要写成如下形式 mov ax,[bar]。由此可见,nasm中对offset的使用也是没有必要的(nasm中无offset)。nasm对[ ]的使用与masm也有所不同,所有的表达式都必须写在[ ]中,下面举两个例子来说明:

masm                        nasm
Mov ax,table[di]            Mov ax,[table+di]

Mov ax,es:[di]              Mov ax,[es:di]

Mov ax,[di]+1               Mov ax,[di+1]


nasm 中不存储变量类型,原因很简单masm中通过[ ]寻址方式的变量也必须要指定类型。nasm中不支持LODS, MOVS, STOS, SCAS, CMPS, INS, OUTS,只支持lodsb、lodsw等已经指定类型的操作。nasm中不再有assume操作,段地址完全取决于存入段寄存器的值。

关于NASM的使用方法及语法还可以参阅NASM使用手册。


结论:

我认为不论是在Windows/DOS下还是在Linux下完完全全用汇编编一个大型程序已经是不可能了,也不会有人愿意去这样做。在windows下我们可以用VC,在Linux/Xwindows下我们可以用C甚至C++ Builder,但是像VC、C++ Builder之类的工具尽量隐藏了底层的调用,同时也阻隔了成为高手的机会,因为编出来的程序无法了解它的执行过程也就使编程中最重要的“可预测”性变得很低。正因为如此汇编才有它存在的必要性,同时还有一个更重要的原因,正如《超级解霸》的作者梁肇新所说:“编程序的重点不是“编”,而是调试程序,理论上的完美在实现的时候会遇到很多细节问题,这些问题必须调试才能解决。我的编程习惯是一天写五天调试,《超级解霸》是调试出来的,而不是写出来的。调试就涉及到汇编的问题,不进行汇编级的调试是不彻底的,也不能让人放心。”


参考资料:

1、Jan’s Assemble Homepage

by Jan Wagemakers 8.1999

2、Linux Assemble HOWTO

by Konstantin Boldyshev and Fran?is-Ren?Rideau 12.1999

3、Linux/i386 System Calls

By Konstantin Boldyshev 1999


联系方法:

这篇文章还可以在 http://toprogram.myrice.com/asm.htm 看到

Email : abc@990.net

Fidonet : 6:653/1003.12
 楼主| 发表于 2004-5-20 09:29:19 | 显示全部楼层

gcc中的内嵌汇编语言

http://www.cooltang.com/box/topi ... ram/nsfocus/065.htm

作者:欧阳光 ouyangguang@263.net

初次接触到AT&T格式的汇编代码,看着那一堆莫名其妙的怪符号,真是有点痛不欲生的感觉,只好慢慢地去啃gcc文档,在似懂非懂的状态下过了一段时间。后来又在网上找到了灵溪写的《gcc中的内嵌汇编语言》一文,读后自感大有裨益。几个月下来,接触的源代码多了以后,慢慢有了一些经验。为了使初次接触AT&T格式的汇编代码的同志不至于遭受我这样的痛苦,就整理出该文来和大家共享。如有错误之处,欢迎大家指正,共同提高。

本文主要以举例的方式对gcc中的内嵌汇编语言进行进一步的解释。

一、gcc对内嵌汇编语言的处理方式

gcc在编译内嵌汇编语言时,采取的步骤如下:

1、变量输入:根据限定符的内容将输入操作数放入合适的寄存器,如果限定符指定为立即数("i")或内存变量("m"),则该步被省略,如果限定符没有具体指定输入操作数的类型(如常用的"g"),gcc会视需要决定是否将该操作数输入到某个寄存器。这样每个占位符都与某个寄存器,内存变量,或立即数形成了一一对应的关系,这就是对第二个冒号后内容的解释。

如:"a"(foo),"i"(100),"m"(bar)表示%0对应eax寄存器,%1对应100,%2对应内存变量bar

2、生成代码:然后根据这种一一对应的关系(还应包括输出操作符),用这些寄存器,内存变量,或立即数来取代汇编代码中的占位符(则有点像宏操作),注意,则一步骤并不检查由这种取代操作所生成的汇编代码是否合法,例如,如果有这样一条指令asm("movl %0,%1"::"m"(foo),"m"(bar));如果你用gcc -c -S选项编译该源文件,那么在生成的汇编文件中,你将会看到生成了movl foo,bar这样一条指令,这显然是错误的。这个错误在稍后的编译检查中会被发现。

3、变量输出:按照输出限定符的指定将寄存器的内容输出到某个内存变量中,如果输出操作数的限定符指定为内存变量("m"),则该步骤被省略。这就是对第一个冒号后内容的解释,如:asm("mov %0,%1":"=m"(foo),"=a"(bar);编译后为

#APP
movl foo,eax

#NO_APP
movl eax,bar

该语句虽然有点怪怪的,但它很好的体现了gcc的运作方式。

再以arch/i386/kernel/apm。c中的一段代码为例,我们来比较一下它们编译前后的情况

源程序

编译后的汇编代码
[php]
__asm__ (

"pushl %%edi\n\t"

"pushl %%ebp\n\t"

"lcall %%cs:\n\t"

"setc %%al\n\t"

"addl %1,%2\n\t"

"popl %%ebp\n\t"

"popl %%edi\n\t"

:"=a"(ea),"=b"(eb),

"=c"(ec),"=d"(ed),"=S"(es)

:"a"(eax_in),"b"(ebx_in),"c"(ecx_in)

:"memory","cc");

movl eax_in,%eax

movl ebx_in,%ebx

movl ecx_in,%ecx

#APP

pushl %edi

pushl %ebp

lcall %cs:

setc %al

addl eb,ec

popl %ebp

popl %edi

#NO_APP

movl %eax,ea

movl %ebx,eb

movl %ecx,ec

movl %edx,ed

movl %esi,es
[/php]

二、对第三个冒号后面内容的解释

第三个冒号后面内容主要针对gcc优化处理,它告诉gcc在本段汇编代码中对寄存器和内存的使用情况,以免gcc在优化处理时产生错误。

1、它可以是"eax","ebx","ecx"等寄存器名,表示本段汇编代码对该寄存器进行了显式操作,如 asm ("mov %%eax,%0",:"=r"(foo)::"eax");这样gcc在优化时会避免使用eax作临时变量,或者避免cache到eax的内存变量通过该段汇编码。

下面的代码均用gcc的-O2级优化,它显示了嵌入汇编中第三个冒号后"eax"的作用

源程序

编译后的汇编代码

正常情况下

int main()
{
  int bar=1;

  bar=fun();

  bar++;

  return bar;
}

  pushl %ebp

  movl %esp,%ebp

  call fun

  incl %eax  #显然,bar缺省使用eax寄存器

  leave

  ret

加了汇编后
int main()
{
  int bar=1;

  bar=fun();

  asm volatile("" : : : "eax");

  bar++;

  return bar;
}

  pushl %ebp

  movl %esp,%ebp #建立堆栈框架

  call fun

#fun的返回值放入bar中,此时由于嵌入汇编

#指明改变了eax的值,为了避免冲突,

#bar改为使用edx寄存器

  movl %eax,%edx

  #APP

  #NO_APP

  incl %edx

  movl %edx,%eax #放入main()的返回值

  leave

  ret

2、"merory"是一个常用的限定,它表示汇编代码以不可预知的方式改变了内存,这样gcc在优化时就不会让cache到寄存器的内存变量使用该寄存器通过汇编代码,否则可能会发生同步出错。有了上面的例子,这个问题就很好理解了


三、对"&"限定符的解释

这是一个较常见用于输出的限定符。它告诉gcc输出操作数使用的寄存器不可再让输入操作数使用。对于"g","r"等限定符,为了有效利用为数不多的几个通用寄存器,gcc一般会让输入操作数和输出操作数选用同一个寄存器。但如果代码没编好,会引起一些意想不到的错误:如

asm("call fun;mov ebx,%1":"=a"(foo):"r"(bar))  ;gcc编译的结果是foo和bar同时使用eax寄存器

movl bar,eax

#APP

call fun

movl ebx,eax

#NO_APP

movl eax,foo

本来这段代码的意图是将fun()函数的返回值放入foo变量,但半路杀出个程咬金,用ebx的值冲掉了返回值,所以这是一段错误的代码,解决的方法是加上一个给输出操作数加上一个"&"限定符:asm("call fun;mov ebx,%1":"=&a"(foo):"r"(bar));这样gcc就会让输入操作数另寻高就,不再使用eax寄存器了
 楼主| 发表于 2004-5-20 09:59:31 | 显示全部楼层

AT&amp;T汇编与Intel汇编的比较

gcc采用的是AT&T的汇编格式,MS采用Intel的汇编格式.

一 基本语法

语法上主要有以下几个不同.

1、寄存器命名原则

AT&T: %eax

Intel: eax

2、源/目的操作数顺序

AT&T: movl %eax,%ebx

Intel: mov ebx,eax


3、常数/立即数的格式

AT&T: movl $_value,%ebx

Intel: mov eax,_value

把_value的地址放入eax寄存器

AT&T: movl $0xd00d,%ebx

Intel: mov ebx,0xd00d

4、操作数长度标识

AT&T: movw %ax,%bx

Intel: mov bx,ax

5、寻址方式

AT&T: immed32(basepointer,indexpointer,indexscale)

Intel: [basepointer + indexpointer*indexscale + imm32)

Linux工作于保护模式下,用的是32位线性地址,所以在计算地址时不用考虑segmentffset的问题.上式中的地址应为:

imm32 + basepointer + indexpointer*indexscale

下面是一些例子:

1、直接寻址

AT&T: _booga ; _booga是一个全局的C变量

注意加上$是表示地址引用,不加是表示值引用.

注:对于局部变量,可以通过堆栈指针引用.

Intel: [_booga]

2、寄存器间接寻址

AT&T: (%eax)

Intel: [eax]

3、变址寻址

AT&T: _variable(%eax)

Intel: [eax + _variable]

AT&T: _array(,%eax,4)

Intel: [eax*4 + _array]

AT&T: _array(%ebx,%eax,8)

Intel: [ebx + eax*8 + _array]


二 基本的行内汇编

基本的行内汇编很简单,一般是按照下面的格式

asm("statements");

例如:

asm("nop");

asm("cli");

asm 和 __asm__是完全一样的.

如果有多行汇编,则每一行都要加上 "\n\t"

例如:

asm( "pushl %eax\n\t"

"movl $0,%eax\n\t"

"popl %eax");

实际上gcc在处理汇编时,是要把asm(...)的内容"打印"到汇编文件中,所以格式控制字符是必要的.

再例如:

asm("movl %eax,%ebx");

asm("xorl %ebx,%edx");

asm("movl $0,_booga);

在上面的例子中,由于我们在行内汇编中改变了edx和ebx的值,但是由于gcc的特殊的处理方法,即先形成汇编文件,再交给GAS去汇编,所以GAS并不知道我们已经改变了edx和ebx的值,如果程序的上下文需要edx或ebx作暂存,这样就会引起严重的后果.对于变量_booga也存在一样的问题.为了解决这个问题,就要用到扩展的行内汇编语法.


三 扩展的行内汇编

扩展的行内汇编类似于Watcom.

基本的格式是:

asm ( "statements" : output_regs : input_regs : clobbered_regs);

clobbered_regs指的是被改变的寄存器.

下面是一个例子(为方便起见,我使用全局变量):

int count=1;

int value=1;

int buf[10];

void main()
{
  asm(

  "cld \n\t"

  "rep \n\t"

  "stosl":: "c" (count), "a" (value) , "D" (buf[0]): "%ecx","%edi" );

}

得到的主要汇编代码为:

movl count,%ecx

movl value,%eax

movl buf,%edi

#APP

cld

rep

stosl

#NO_APP

cld,rep,stos就不用多解释了.这几条语句的功能是向buf中写上count个value值.冒号后的语句指明输入,输出和被改变的寄存器.通过冒号以后的语句,编译器就知道你的指令需要和改变哪些寄存器,从而可以优化寄存器的分配.其中符号"c"(count)指示要把count的值放入ecx寄存器

类似的还有:

a eax

b ebx

c ecx

d edx

S esi

D edi

I 常数值,(0 - 31)

q,r 动态分配的寄存器

g eax,ebx,ecx,edx或内存变量

A 把eax和edx合成一个64位的寄存器(use long longs)


我们也可以让gcc自己选择合适的寄存器.如下面的例子:

asm("leal (%1,%1,4),%0"

: "=r" (x)

: "0" (x) );

这段代码实现5*x的快速乘法.

得到的主要汇编代码为:

movl x,%eax

#APP

leal (%eax,%eax,4),%eax

#NO_APP

movl %eax,x

几点说明:

1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器.使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器.

2.我们不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们.

3."="是标示输出寄存器,必须这样用.

4.数字%n的用法:数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求的寄存器.如果我们要重用"r"或"q"请求的寄存器的话,就可以使用它们.

5.如果强制使用固定的寄存器的话,如不用%1,而用ebx,则

asm("leal (%%ebx,%%ebx,4),%0"

: "=r" (x)

: "0" (x) );

注意要使用两个%,因为一个%的语法已经被%n用掉了.

下面可以来解释letter 4854-4855的问题:

1、变量加下划线和双下划线有什么特殊含义吗?加下划线是指全局变量,但我的gcc中加不加都无所谓.

2、以上定义用如下调用时展开会是什么意思?

#define _syscall1(type,name,type1,arg1) \

type name(type1 arg1) \

{ \

long __res; \

/* __res应该是一个全局变量 */

__asm__ volatile ("int $0x80" \

/* volatile 的意思是不允许优化,使编译器严格按照你的汇编代码汇编*/

: "=a" (__res) \

/* 产生代码 movl %eax, __res */

: "0" (__NR_##name),"b" ((long)(arg1))); \

/* 如果我没记错的话,这里##指的是两次宏展开.

  即用实际的系统调用名字代替"name",然后再把__NR_...展开.

  接着把展开的常数放入eax,把arg1放入ebx */

if (__res >= 0) \

return (type) __res; \

errno = -__res; \

return -1; \

}
 楼主| 发表于 2004-5-20 10:01:20 | 显示全部楼层

gcc中的内嵌汇编语言(Intel i386平台)

作 者:  吴亮

一.声明

虽然Linux的核心代码大部分是用C语言编写的,但是不可避免的其中还是有一部分是用汇编语言写成的。有些汇编语言代码是直接写在汇编源程序中的,特别是Linux的启动代码部分;还有一些则是利用gcc的内嵌汇编语言嵌在C语言程序中的。这篇文章简单介绍了gcc中的内嵌式汇编语言,主要想帮助那些才开始阅读Linux核心代码的朋友们能够更快的入手。写这篇文章的主要信息来源是GNU的两个info文件:as.info和gcc.info,如果你觉得这篇文章中的介绍还不够详细的话,你可以查阅这两个文件。当然,直接查阅这两个文件可以获得更加权威的信息。如果你不想被这两篇文档中的一大堆信息搞迷糊的话,我建议你先阅读一下这篇文章,然后在必要时再去查阅更权威的信息。

二.简介

在Linux的核心代码中,还是存在相当一部分的汇编语言代码。如果你想顺利阅读Linux代码的话,你不可能绕过这一部分代码。在Linux使用的汇编语言代码中,主要有两种格式:一种是直接写成汇编语言源程序的形式,这一部分主要是一些Linux的启动代码;另一部分则是利用gcc的内嵌式汇编语言语句asm嵌在Linux的C语言代码中的。这篇文章主要是介绍第二种形式的汇编语言代码。首先,我介绍一下as支持的汇编语言的语法格式。大家知道,我们现在学习的汇编语言的格式主要是Intel风格的,而在Linux的核心代码中使用的则是AT&T格式的汇编语言代码,应该说大部分人对这种格式的汇编语言还不是很了解,所以我觉得有必要介绍一下。接着,我主要介绍一下gcc的内嵌式汇编语言的格式。gcc的内嵌式汇编语言提供了一种在C语言源程序中直接嵌入汇编指令的很好的办法,既能够直接控制所形成的指令序列,又有着与C语言的良好接口,所以在Linux代码中很多地方都使用了这一语句。

三.AT&T的汇编语言语法格式

我想我们大部分人对Intel格式的汇编语言都很了解了。但是,在Linux核心代码中,所有的汇编语言指令都是用AT&T格式的汇编语言书写的。这两种汇编语言在语法格式上有着很大的不同:

1.在AT&T的汇编语言中,用'$'前缀表示一个立即操作数;而在Intel的格式中,立即操作数的表示不带任何前缀符。例如:下面两个语句是完全相同的:

*AT&T: pushl $4
*Intel: push 4

2.AT&T和Intel的汇编语言格式中,源操作数和目标操作数的位置正好相反。Intel的汇编语言中,目标操作数在源操作数的左边;而在AT&T的汇编语言中,目标操作数则在源操作数的右边。例如:

*AT&T : addl $4,%eax
*Intel: add eax,4

3.在AT&T的汇编语言中,操作数的字长是由操作码助记符的最后一个字母决定的,后缀'b'、'w'、'l'分别表示操作数的字长为8比特(字节,byte),16比特(字,word)和32比特(长字,long),而Intel格式中操作数的字长是用“word ptr”或者“byte ptr”等前缀来表示的。例如:

*AT&T: movb FOO,%al
*Intel: mov al,byte ptr FOO

4.在AT&T汇编指令中,直接远跳转/调用的指令格式是“lcall/ljmp $SECTION,$OFFSET”,同样,远程返回的指令是“lret  $STACK-ADJUST”;而在Intel格式中,相应的指令分别为“call/jmp far SECTION:OFFSET”和“ret far STACK-ADJUST”。

(1)AT&T汇编指令操作助记符命名规则AT&T汇编语言中,操作码助记符的后缀字符指定了该指令中操作数的字长。后缀字母'b'、'w'、'l'分别表示字长为8比特(字节,byte),16比特(字,word)和32比特(长字,long)的操作数。如果助记符中没有指定字长后缀并且该指令中没有内存操作数,汇编程序'as'会根据指令中指定的寄存器操作数补上相应的后缀字符。所以,下面的两个指令具有相同的效果(这只是GNU的汇编程序as的一个特性,AT&T的Unix汇编程序将没有字长后缀的指令的操作数字长假设为32比特):

mov %ax,%bx
movw %ax,%bx

AT&T中几乎所有的操作助记符与Intel格式中的助记符同名,仅有一小部分例外。操作数扩展指令就是例外之一。在AT&T汇编指令中,操作数扩展指令有两个后缀:一个指定源操作数的字长,另一个指定目标操作数的字长。AT&T的符号扩展指令的基本助记符为'movs',零扩展指令的基本助记符为'movz'(相应的Intel指令为'movsx'和'movzx')。因此,'movsbl %al,%edx'表示对寄存器al中的字节数据进行字节到长字的符号扩展,计算结果存放在寄存器edx中。下面是一些允许的操作数扩展后缀:

*bl: 字节->长字
*bw: 字节->字
*wl: 字->长字

还有一些其他的类型转换指令的对应关系:

*Intel *AT&T
⑴ cbw cbtw
符号扩展:al->ax
⑵ cwde cwtl
符号扩展:ax->eax
⑶ cwd cwtd
符号扩展:ax->dx:ax
⑷ cdq cltd
符号扩展:eax->edx:eax

还有一个不同名的助记符就是远程跳转/调用指令。在Intel格式中,远程跳转/调用指令的助记符为“call/jmp far”,而在AT&T的汇编语言中,相应的指令为“lcall”和“ljmp”。

(2)AT&T中寄存器的命名在AT&T汇编语言中,寄存器操作数总是以'%'作为前缀。80386芯片的寄存器包括:

⑴8个32位寄存器:'%eax','%ebx','%ecx','%edx','%edi','%esi','%ebp','%esp'
⑵8个16位寄存器:'%ax','%bx','%cx','%dx','%si','%di','%bp','%sp'
⑶8个8位寄存器:'%ah','%al','%bh','%bl','%ch','%cl','%dh','%dl'
⑷6个段寄存器:'%cs','%ds','%es','%ss','%fs','%gs'
⑸3个控制寄存器:'%cr0','%cr1','%cr2'
⑹6个调试寄存器:'%db0','%db1','%db2','%db3','%db6','%db7'
⑺2个测试寄存器:'%tr6','%tr7'
⑻8个浮点寄存器栈:'%st(0)','%st(1)','%st(2)','%st(3)','%st(4)','%st(5)','%st(6)','%st(7)'

*注:我对这些寄存器并不是都了解,这些资料只是摘自as.info文档。如果真的需要寄存器命名的资料,我想可以参考一下相应GNU工具的机器描述方面的源文件。

(3)AT&T中的操作码前缀

⑴段超越前缀'cs','ds','es','ss','fs','gs':当汇编程序中对内存操作数进行SECTION:MEMORY-OPERAND引用时,自动加上相应的段超越前缀。
⑵操作数/地址尺寸前缀'data16','addr16':这些前缀将32位的操作数/地址转化为16位的操作数/地址。
⑶总线锁定前缀'lock':总线锁定操作。'lock'前缀在Linux核心代码中使用很多,特别是SMP代码中。
⑷协处理器等待前缀'wait':等待协处理器完成当前操作。
⑸指令重复前缀'rep','repe','repne':在串操作中重复指令的执行。

(4)AT&T中的内存操作数在Intel的汇编语言中,内存操作数引用的格式如下:

SECTION:[BASE + INDEX*SCALE + DISP]

而在AT&T的汇编语言中,内存操作数的应用格式则是这样的:

SECTIONISP(BASE,INDEX,SCALE)

下面是一些内存操作数的例子:
*AT&T *Intel
⑴ -4(%ebp) [ebp-4]
⑵ foo(,%eax,4) [foo+eax*4]
⑶ foo(,1) [foo]
⑷ %gs:foo gs:foo

还有,绝对跳转/调用指令中的内存操作数必须以'*'最为前缀,否则as总是假设这是一个相对跳转/调用指令。

(5)AT&T中的跳转指令

as汇编程序自动对跳转指令进行优化,总是使用尽可能小的跳转偏移量。如果8比特的偏移量无法满足要求的话,as会使用一个32位的偏移量,as汇编程序暂时还不支持16位的跳转偏移量,所以对跳转指令使用'addr16'前缀是无效的。

还有一些跳转指令只支持8位的跳转偏移量,这些指令包括:'jcxz','jecxz','loop','loopz','loope','loopnz'和'loopne'。所以,在as的汇编源程序中使用这些指令可能会出错。(幸运的是,gcc并不使用这些指令)

对AT&T汇编语言语法的简单介绍差不多了,其中有些特性是as特有的。在Linux核心代码中,并不涉及到所有上面这些提到的语法规则,其中有两点规则特别重要:第一,as中对寄存器引用时使用前缀'%';第二,AT&T汇编语言中源操作数和目标操作数的位置与我们熟悉的Intel的语法正好相反。

四.gcc的内嵌汇编语言语句asm利用gcc的asm语句,你可以在C语言代码中直接嵌入汇编语言指令,同时还可以使用C语言的表达式指定汇编指令所用到的操作数。这一特性提供了很大的方便。

要使用这一特性,首先要写一个汇编指令的模板(这种模板有点类似于机器描述文件中的指令模板),然后要为每一个操作数指定一个限定字符串。例如:
extern __inline__ void change_bit(int nr,volatile void *addr)
{
__asm__ __volatile__( LOCK_PREFIX
"btcl %1,%0"
:"=m" (ADDR)
:"ir" (nr));
}

上面的函数中:
LOCK_PREFIX:这是一个宏,如果定义了__SMP__,扩展为"lock;",用于指定总线锁定前缀,否则扩展为""。
ADDR:这也是一个宏,定义为(*(volatile struct __dummy *) addr)"btcl %1,%0":这就是嵌入的汇编语言指令,btcl为指令操作码,%1,
%0是这条指令两个操作数的占位符。后面的两个限定字符串就用于描述这两个操作数。
: "=m" (ADDR):第一个冒号后的限定字符串用于描述指令中的“输出”操作数。刮号中的ADDR将操作数与C语言的变量联系起来。这个限定字符串表示指令中的“%0”就是addr指针指向的内存操作数。这是一个“输出”类型的内存操作数。
: "ir" (nr):第二个冒号后的限定字符串用于描述指令中的“输入”操作数。这条限定字符串表示指令中的“%1”就是变量nr,这个的操作数可以是一个立即操作数或者是一个寄存器操作数。

*注:限定字符串与操作数占位符之间的对应关系是这样的:在所有限定字符串中(包括第一个冒号后的以及第二个冒号后的所有限定字符串),最先出现的字符串用于描述操作数“%0”,第二个出现的字符串描述操作数“%1”,以此类推。

(1)汇编指令模板

asm语句中的汇编指令模板主要由汇编指令序列和限定字符串组成。在一个asm语句中可以包括多条汇编指令。汇编指令序列中使用操作数占位符引用C语言中的变量。一条asm语句中最多可以包含十个操作数占位符:%0,%1,...,%9。汇编指令序列后面是操作数限定字符串,对指令序列中的占位符进行限定。限定的内容包括:该占位符与哪个C语言变量对应,可以是什么类型的操作数等等。限定字符串可以分为三个部分:输出操作数限定字符串(指令序列后第一个冒号后的限定字符串),输入操作数限定字符串(第一个冒号与第二个冒号之间),还有第三种类型的限定字符串在第二个冒号之后。同一种类型的限定字符串之间用逗号间隔。asm语句中出现的第一个限定字符串用于描述占位符“%0”,第二个用于描述占位符“%1”,以此类推(不管该限定字符串的类型)。如果指令序列中没有任何输出操作数,那么在语句中出现的第一个限定字符串(该字符串用于描述输入操作数)之前应该有两个冒号(这样,编译器就知道指令中没有输出操作数)。指令中的输出操作数对应的C语言变量应该具有左值类型,当然对于输出操作数没有这种左值限制。输出操作数必须是只写的,也就是说,asm对取出某个操作数,执行一定计算以后再将结果存回该操作数这种类型的汇编指令的支持不是直接的,而必须通过特定的格式的说明。如果汇编指令中包含了一个输入-输出类型的操作数,那么在模板中必须用两个占位符对该操作数的不同功能进行引用:一个负责输入,另一个负责输出。例如:

asm ("addl %2,%0":"=r"(foo):"0"(foo),"g"(bar));

在上面这条指令中,“%0”是一个输入-输出类型的操作数,"=r"(foo)用于限定其输出功能,该指令的输出结果会存放到C语言变量foo中;指令中没有显式的出现“%1”操作数,但是针对它有一个限定字符串"0"(foo),事实上指令中隐式的“%1”操作数用于描述“%0”操作数的输入功能,它的限定字符串中的"0"限定了“%1”操作数与“%0”具有相同的地址。可以这样理解上述指令中的模板:该指令将“%1”和“%2”中的值相加,计算结果存放回“%0”中,指令中的“%1”与“%0”具有相同的地址。注意,用于描述“%1”的"0"限定字符足以保证“%1”与“%0”具有相同的地址。但是,如果用下面的指令完成这种输入-输出操作就不会正常工作:

asm ("addl %2,%0":"=r"(foo):"r"(foo),"g"(bar));

虽然该指令中“%0”和“%1”同样引用了C语言变量foo,但是gcc并不保证在生成的汇编程序中它们具有相同的地址。还有一些汇编指令可能会改变某些寄存器的值,相应的汇编指令模板中必须将这种情况通知编译器。所以在模板中还有第三种类型的限定字符串,它们跟在输入操作数限定字符串的后面,之间用冒号间隔。这些字符串是某些寄存器的名称,代表该指令会改变这些寄存器中的内容。

在内嵌的汇编指令中可能会直接引用某些硬件寄存器,我们已经知道AT&T格式的汇编语言中,寄存器名以“%”作为前缀,为了在生成的汇编程序中保留这个“%”号,在asm语句中对硬件寄存器的引用必须用“%%”作为寄存器名称的前缀。如果汇编指令改变了硬件寄存器的内容,不要忘记通知编译器(在第三种类型的限定串中添加相应的字符串)。还有一些指令可能会改变CPU标志寄存器EFLAG的内容,那么需要在第三种类型的限定字符串中加入"cc"。为了防止gcc在优化过程中对asm中的汇编指令进行改变,可以在"asm"关键字后加上"volatile"修饰符。可以在一条asm语句中描述多条汇编语言指令;各条汇编指令之间用“;”或者“\n”隔开。

(2)操作数限定字符

操作数限定字符串中利用规定的限定字符来描述相应的操作数,一些常用的限定字符有:(还有一些没有涉及的限定字符,参见gcc.info)
1。"m":操作数是内存变量。
2。"o":操作数是内存变量,但它的寻址方式必须是“偏移量”类型的,
也就是基址寻址或者基址加变址寻址。
3。"V":操作数是内存变量,其寻址方式非“偏移量”类型。
4。" ":操作数是内存变量,其地址自动增量。
6。"r":操作数是通用寄存器。
7。"i":操作数是立即操作数。(其值可在汇编时确定)
8。"n":操作数是立即操作数。有些系统不支持除字(双字节)以外的
立即操作数,这些操作数要用"n"而不是"i"来描述。
9。"g":操作数可以是立即数,内存变量或者寄存器,只要寄存器属
于通用寄存器。
10。"X":操作数允许是任何类型。
11。"0","1",...,"9":操作数与某个指定的操作数匹配。也就是说,
该操作数就是指定的那个操作数。例如,如果用"0"来描述"%1"操作
数,那么"%1"引用的其实就是"%0"操作数。
12。"p":操作数是一个合法的内存地址(指针)。
13。"=":操作数在指令中是只写的(输出操作数)。
14。"+":操作数在指令中是读-写类型的(输入-输出操作数)。
15。"a":寄存器EAX。
16。"b":寄存器EBX。
17。"c":寄存器ECX。
18。"d":寄存器EDX。
19。"q":寄存器"a","b","c"或者"d"。
20。"A":寄存器"a"或者"d"。
21。"a":寄存器EAX。
22。"f":浮点数寄存器。
23。"t":第一个浮点数寄存器。
24。"u":第二个浮点数寄存器。
25。"D":寄存器di。
26。"S":寄存器si。
27。"I":0-31之间的立即数。(用于32位的移位指令)
28。"J":0-63之间的立即数。(用于64位的移位指令)
29。"N":0-255之间的立即数。(用于"out"指令)
30。"G":标准的80387浮点常数。

*注:还有一些不常见的限定字符并没有在此说明,另外有一些限定字符,例如"%","&"等由于我缺乏编译器方面的一些知识,所以我也不是很理解它们的含义,如果有高手愿意补充,不慎感激!不过在核心代码中出现的限定字符差不多就是上面这些了。

吴亮
E-mail:kotama@163.com
QQ:35377028
发表于 2004-6-25 23:19:26 | 显示全部楼层
把sunheart兄弟发的几个有关Linux下汇编的贴子全并成一个贴子,顺便整理了一下。由于仓促,希望阅读本贴的兄弟发现错误跟贴,以便改正。

谢谢!
发表于 2004-6-27 21:30:47 | 显示全部楼层
不错啊,支持一下!-
发表于 2005-4-6 19:55:12 | 显示全部楼层
不错
支持一下
回复 支持 反对

使用道具 举报

发表于 2005-4-8 15:12:44 | 显示全部楼层
收藏!好贴!
回复 支持 反对

使用道具 举报

发表于 2009-10-28 06:56:00 | 显示全部楼层
支持一下~~~
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表