分享

Linux汇编语言及嵌入式汇编

 lifei_szdz 2012-04-25

Linux汇编语言及嵌入式汇编

(2008-04-28 10:34:18)
标签:

杂谈

分类: MSN搬家
汇编语言的优点是速度快,可以直接对硬件进行操作。Linux是用C语言开发的操作系统,可以在Linux中直接使用汇编这一底层语言来优化程序的性能。
汇编语言虽然运用不像高级语言那么广泛,但是却很重要。特别是在一些执行速度要求很高的场合,如Linux这样的操作系统。Linux中引导程序、启动程序及内核程序中都有很多汇编程序或嵌入式汇编程序。
汇编语言具有如下优点:
能够直接访问与硬件相关的存储器和I/O端口
不受编译器限制,对生成的二进制代码进行完全控制
能够对关键代码进行控制,避免因线程共同范围或硬件设备共享引起的死锁
对代码进行优化,提高执行速度
同时,汇编语言也有不容忽视的缺点:
代码难懂,不易维护
 容易产生bug,调试困难
只能针对特定的体系结构和处理器进行优化
 开放效率低,周期长
Linux下用汇编语言编写的代码有两种形式:一种是完全的汇编代码,另外一种是内嵌的汇编代码,即可以内嵌到C语言的汇编代码片段,这主要依赖于编译器在这方面的扩展。
 
Linux汇编语言格式
不同于DOS/Windows下的Intel风格的汇编语言,Unix/Linux下的汇编语法风格主要是AT&T模式的。
1.        AT&T汇编格式中,寄存器名前要加上“%”作为前缀。

AT&T格式

Intel格式

push %eax

push eax

2.        AT&T汇编格式中,用‘$’前缀表示一个立即操作数;在Intel汇编格式中,立即数的表示不用带任何前缀。

AT&T格式

Intel格式

pushl $1

push 1

3.        AT&TIntel格式中的源操作数和目的操作数的位置恰好相反。在Intel格式中,目的操作数在左边;而在AT&T格式中,目的操作数在右边。

AT&T格式

Intel格式

addl $1, %eax

add eax, 1

4.        AT&T格式中,操作数的长度由操作符的最后一个字母决定,后缀‘b’w’l’分别表示操作数为字节(byte, 8bits),字(word, 16bits)和长字(long, 32bits);而在Intel格式中,操作数的字长由“byte ptr”和“word ptr”等前缀来表示。

AT&T格式

Intel格式

movb val, %al

mov al, byte ptr val

 
1.        AT&T汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上‘*’
最为前缀,而在Intel格式中则不需要。
2.        远程转移指令和远程子调用指令的操作码,在AT&T汇编格式中为‘ljump’和‘lcall’;而在Intel汇编格式中为‘jmp far’和‘call far’


AT&T格式

Intel格式

ljump $section, $offset

jmp far section:offset

lcall $section, $offset

call far section:offset

   与之对应的远程返回指令为:

AT&T格式

Intel格式

lret $stack_adjust

ret far stack_adjust

3.        AT&T汇编格式中,内存操作数的寻址方式是


section:disp(base, index, scale)
 
 而在Intel汇编格式中,内存操作数的寻址方式是:

 



section:[base + index* scale + disp]
 
 
由于Linux工作在保护模式下,用的是32位线性地址,所以在计算地址时不用考虑段基址和偏移量,采用如下的计算方法:

 



disp + base + index * scale
           
 
下面是一些内存操作数的例子:

AT&T格式

Intel格式

movl –4(%ebp), %eax

mov eax, [ebp-4]

movl array(,%eax, 4), %eax

mov eax,[eax*4 + array]

movw array(%ebx, %eax, 4), %cx

mov cx,[ebx + 4*eax + array]

movb $4, %fs:(%eax)

mov fs:eax, 4

 
Hello World!
Linux下有很多方法用于在屏幕上显示一个字符串,但最简洁的方式是使用Linux内核提供的系统调用。这种方法可以直接和操作系统内核进行通讯,不需要链接如libc这样的函数库,也不需要ELF解释器。Linux是一个运行于保护模式下的32位操作系统,采用平坦内存模式,最常用到的ELF二进制代码格式包含.text, .data.bsssection。其中.text为只读代码区,.data为可读可写的数据区,而.bss为可读可写且没有初始化的数据区。一个ELF可执行程序最少应该包含.text部分。

AT&T格式的Hello, World



#hello.s
.data                                #数据段声明
msg .
string “hello, world!
”        #要输出的字符串
len 
=
 – msg                     #字符串长度
.text                                #代码段声明
.
global
 _start                         #指定入口函数
_start:
movl $len, 
%
edx                 
movl $msg, 
%
ecx
movl $
1%
ebx                   #文件描述符stdout
movl $
4%
eax                   #系统调用号(sys_write)
int $0x80
                       #调用内核功能
                                   #退出程序
movl $
0%
ebx                     #退出代码
movl $
1%
eax                     #系统调用号(sys_exit)
int $0x80

     Intel格式的Hello, World!

 

hello.asm
section .data                     ;数据段声明
msg db “Hello, world
!”, 0xA
   
len equ 
-
msg 
section .text
global
 _start
_start:
mov edx, len
mov ecx, msg
mov ebx, 
1

mov eax, 
4
int 0x80

mov ebx, 
0
mov eax, 
1
int 0x80

  上面两种汇编格式都是Linux内核提供的sys_write来显示一个字符串,然后再调用sys_exit退出程序。可以再include/asm-i386/unistd.h中,找到系统调用的定义。
Linux汇编工具
Linux平台下汇编工具仍然是汇编器、链接器和调试器
汇编器
汇编器(assembler)作用是将汇编语言编写的源程序转换成二进制形式的目标代码。Linux平台的标准汇编器是GAS,使用AT&T汇编语法。
     as –o hello.o hello.s
Linux平台下另一个常用到的汇编器是NASM,提供了很好的宏指令功能,并支持相当多的目标代码格式,包括bin, a.out, coff, elf, rdf等。NASM使用Intel汇编语法。
链接器
链接器用来将多个目标代码连接成一个可执行代码。Linux下使用ld作为标准的链接程序。
      ld –s –o hello hello.o
调试器
Linux下汇编代码既可以用GDBGDD等通用调试器,也可以用专门用来调试汇编代
码的ALD(Assembly Language Debugger)
 
系统调用
程序中一般都会用到输入、输出及退出等操作。这些操作可通过系统调用来完成,即通过调用操作系统提供的服务来完成。Linux下可通过封装的C(libc)或者汇编直接调用。通过系统调用的方法高效,因为最终生成的程序不需要与任何库进行链接,而是直接和内核通信。与DOS一样,Linux下系统调用也是通过中断(int 0x80)来完成。系统功能号在eax中,传递的参数使用寄存器参数传递,按顺序存放在ebx, ecx, edx, esi, edi中,调用完毕,返回值在eax中。系统功能号在/usr/includes/bits/syscall.h中,可用SYS_<name>宏来定义。当系统调用的参数个数大于5时,系统调用功能号仍然保存在eax中,全部参数依次保存在一块连续的内存区中,同时寄存器ebx保存指向该内存区域的指针。返回值保存在寄存器eax中。可以像普通函数调用一样使用栈(stack)来传递系统调用的参数。Linux采用c语言的调用模式,所有的参数必须以相反的顺序进栈,即最后一个参数先入栈,第一个参数最后进栈。如果采用栈来传递系统调用的所需的参数,执行int 0x80时将栈指针复制到寄存器ebx中。
 
命令行参数
Linux操作系统中,一个可执行程序通过命令行启动时,所需的参数保存在栈中:
首先是argc,然后是指向各个命令行参数的指针数组argv,最后是指向环境变量的
指针数据envp



#args.s
.text
.
global _start
_start:
popl 
%ecx         #argc
vnext:
popl 
%ecx          #argv
test 
%ecx, %ecx     #空指针表明结束
jz exit
movl 
%ecx, %ebx
xorl 
%edx, %edx
strlen:
movb (
%ebx), %al
inc 
%edx
inc 
%ebx
test 
%al, %al
jnz strlen
movb $
10-1(%ebx)
movl $
4%eax         #sys_write
movl $
1%ebx
int $0x80
jmp vnext
exit:
movl $
1%eax          #系统调用号sys_exit
xorl 
%ebx, %ebx
int 0x80

ret

 
Linux嵌入式汇编
嵌入式汇编的基本格式是:
          asm(“汇编语句
:输出寄存器
:输入寄存器
:会被修改的寄存器);
其中,汇编语句是放汇编语言的地方;输出寄存器表示那些寄存器存放数据,这些寄存器分别对应一个C语言表达式或一个内存地址;输入寄存器表示开始执行汇编代码时,指定的一些寄存器中应存放的输入值,也分别对应一个C变量或常数值。


#define get_seg_byte(seg, addr) 
(
{
  register 
char __res; 
  __asm__(“push 
%%fs; 
           mov  
%%ax, %%fs; 
           movb 
%%fs:%2%%al;
           pop 
%%fs”
           :”
=a”(__res)
           :”””(seg), “m”(
*(addr)));
res;}
)

 
嵌入汇编语言宏函数常作为一个宏,用圆括号括住的组合语句(花括号中的语句)可以作为表达式使用。:”=a”(__res)表示代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值,”=a”中的”a”叫加载码,”=”表示这是一个输出寄存器。”””(seg), “m”(*(addr)))为输入寄存器,其中”””表示使用与上面同个位置的输出寄存器。而”m”(*(addr))表示一个内存偏移地址值。为了在汇编语句中使用该地址值,嵌入式汇编规定把输入和输出寄存器统一按顺序编号,顺序是从输出寄存器从左到右从上到下以”%0”开始,依次记为%0, %1, %2
常用寄存器加载代码说明
 


代码

说明

代码

说明

a

eax

m

使用内存地址

b

ebx

o

使用内存地址并加偏移值

c

ecx

I

使用常数0-31

d

edx

J

使用常数0-63

S

使用esi

K

使用常数0-255

D

使用edi

L

使用常数0-65535

q

使用动态分配字节可寻址寄存器(eax, ebx, ecx或edx)

M

使用常数0-3

r

使用任意动态分配的寄存器

N

使用1字节常数(0-255)

g

使用通用有效的地址即可(eax, ebx, ecx, edx或内存变量)

O

使用常数0-31

A

使用eax与edx联合(64位)

 

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多