鱼C论坛

 找回密码
 立即注册
查看: 1997|回复: 4

[技术交流] AT&T汇编语言的相关知识

[复制链接]
发表于 2014-1-18 20:51:19 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 catch 于 2014-1-18 20:53 编辑

Linux源代码中,以.S为扩展名的文件是“纯”汇编语言的文件。这里,我们结合具体的例子再介绍一些AT&T汇编语言的相关知识。

1.GNU汇编程序GAS(GNU Assembly和连接程序
当你编写了一个程序后,就需要对其进行汇编(assembly)和连接。在Linux下有两种方式,一种是使用汇编程序GAS和连接程序ld,一种是使用gcc。我们先来看一下GAS和ld:
GAS把汇编语言源文件(.o)转换为目标文件(.o),其基本语法如下:
as filename.s -o filename.o
一旦创建了一个目标文件,就需要把它连接并执行,连接一个目标文件的基本语法为:
ld filename.o -o filename
这里 filename.o是目标文件名,而filename 是输出(可执行) 文件。
GAS使用的是AT&T的语法而不是Intel的语法,这就再次说明了AT&T语法是Unix世界的标准,你必须熟悉它。
如果要使用GNC的C编译器gcc,就可以一步完成汇编和连接,例如:
gcc -o example example.S
这里,example.S是你的汇编程序,输出文件(可执行文件)名为example。其中,扩展名必须为大写的S,这是因为,大写的S可以使gcc自动识别汇编程序中的C预处理命令,像#include#define#ifdef #endif等,也就是说,使用gcc进行编译,你可以在汇编程序中使用C的预处理命令。






想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2014-1-18 20:58:43 | 显示全部楼层
2.  AT&T中的节(Section)
     在AT&T的语法中,一个节由.section关键词来标识,当你编写汇编语言程序时,至少需要有以下三种节:
.section .data: 这种节包含程序已初始化的数据,也就是说,包含具有初值的那些变量,例如:
              hello     : .string "Hello world!\n"
              hello_len : .long 13

.section .bss:这个节包含程序还未初始化的数据,也就是说,包含没有初值的那些变量。当操作
  系统装入这个程序时将把这些变量都置为0,例如:
      name      : .fill 30   # 用来请求用户输入名字
             name_len  : .long  0   # 名字的长度 (尚未定义)
       当这个程序被装入时,name 和 name_len都被置为0。如果你在.bss节不小心给一个变量赋了初值,这个值也会丢失,并且变量的值仍为0。
使用.bss比使用.data的优势在于,.bss节不占用磁盘的空间。在磁盘上,一个长整数就足以存放.bss节。当程序被装入到内存时,操作系统也只分配给这个节4个字节的内存大小。
注意:编译程序把.data和.bss在4字节上对齐(align),例如,.data总共有34字节,那么编译程序把它对其在36字节上,也就是说,实际给它36字节的空间。
.section .text :这个节包含程序的代码,它是只读节,而.data 和.bss是读/写节。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:01:09 | 显示全部楼层

3.汇编程序指令(Assembler Directive)
    上面介绍的.section就是汇编程序指令的一种,GNU汇编程序提供了很多这样的指令(directiv),这种指令都是以句点(.)为开头,后跟指令名(小写字母),在此,我们只介绍在内核源代码中出现的几个指令(以arch/i386/kernel/head.S中的代码为例)。
(1)ascii "string"...
.ascii 表示零个或多个(用逗号隔开)字符串,并把每个字符串(结尾不自动加“0“字节)中的字符放在连续的地址单元。
还有一个与.ascii类似的.asciz,z代表“0“,即每个字符串结尾自动加一个”0“字节,例如:
int_msg:
         .asciz "Unknown interrupt\n"
(2).byte 表达式
     .byte表示零或多个表达式(用逗号隔开),每个表达式被放在下一个字节单元。
(3).fill 表达式
     形式:.fill repeat , size , value
     其中,repeat、size 和value都是常量表达式。Fill的含义是反复拷贝size个字节。Repeat可以大于等于0。size也可以大于等于0,但不能超过8,如果超过8,也只取8。把repeat个字节以8个为一组,每组的最高4个字节内容为0,最低4字节内容置为value。
     Size和 value为可选项。如果第二个逗号和value值不存在,则假定value为0。如果第一个逗号和size不存在,则假定size为1。
     例如,在Linux初始化的过程中,对全局描述符表GDT进行设置的最后一句为:
     .fill NR_CPUS*4,8,0             /* space for TSS's and LDT's */
     因为每个描述符正好占8个字节,因此,.fill给每个CPU留有存放4个描述符的位置。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:02:50 | 显示全部楼层
catch 发表于 2014-1-18 21:01
3.汇编程序指令(Assembler Directive)    上面介绍的.section就是汇编程序指令的一种,GNU汇编程序提供 ...

(4).globl symbol
     .globl使得连接程序(ld)能够看到symbl。如果你的局部程序中定义了symbl,那么,与这个局部程序连接的其他局部程序也能存取symbl,例如:
     .globl SYMBOL_NAME(idt)
     .globl SYMBOL_NAME(gdt)
     定义idt和gdt为全局符号。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:04:38 | 显示全部楼层
catch 发表于 2014-1-18 21:02
(4).globl symbol     .globl使得连接程序(ld)能够看到symbl。如果你的局部程序中定义了symbl,那么, ...

(5)quad bignums
.quad表示零个或多个bignums(用逗号分隔),对于每个bignum,其缺省值是8字节整数。如果bignum超过8字节,则打印一个警告信息;并只取bignum最低8字节。
例如,对全局描述符表的填充就用到这个指令:
.quad 0x00cf9a000000ffff        /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff        /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff        /* 0x23 user   4GB code at 0x00000000 */
.quad 0x00cff2000000ffff        /* 0x2b user   4GB data at 0x00000000 */

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:05:28 | 显示全部楼层
catch 发表于 2014-1-18 21:04
(5)quad bignums.quad表示零个或多个bignums(用逗号分隔),对于每个bignum,其缺省值是8字节整数。如 ...

(6)rept count
     把.rept指令与.endr指令之间的行重复count次,例如
        .rept   3
        .long   0
        .endr
    相当于
        .long   0
        .long   0
        .long   0
(7)space size , fill
  这个指令保留size个字节的空间,每个字节的值为fill。size 和fill都是常量表达式。如果逗号和fill被省略,则假定fill为0,例如在arch/i386/bootl/setup.S中有一句:
  .space  1024
  表示保留1024字节的空间,并且每个字节的值为0。
(8).word expressions
   这个表达式表示任意一节中的一个或多个表达式(用逗号分开),表达式的值占两个字节,例如:
  gdt_descr:
        .word GDT_ENTRIES*8-1
   表示变量gdt_descr的置为GDT_ENTRIES*8-1
(9).long expressions
    这与.word类似
(10).org new-lc , fill
    把当前节的位置计数器提前到new-lc(new location counter)。new-lc或者是一个常量表达式,或者是一个与当前子节处于同一节的表达式。也就是说,你不能用.org横跨节:如果new-lc是个错误的值,则.org被忽略。.org只能增加位置计数器的值,或者让其保持不变;但绝不能用.org来让位置计数器倒退。
   注意,位置计数器的起始值是相对于一个节的开始的,而不是子节的开始。当位置计数器被提升后,中间位置的字节被填充值fill(这也是一个常量表达式)。如果逗号和fill都省略,则fill的缺省值为0。
  例如:.org 0x2000
        ENTRY(pg0)
  表示把位置计数器置为0x2000,这个位置存放的就是临时页表pg0。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:10:28 | 显示全部楼层
catch 发表于 2014-1-18 21:05
(6)rept count     把.rept指令与.endr指令之间的行重复count次,例如        .rept   3        .long  ...

在Linux的源代码中,有很多C语言的函数中嵌入一段汇编语言程序段,这就是gcc提供的“asm”功能,例如在include/asm-i386/system.h中定义的,读控制寄存器CR0的一个宏read_cr0():
#define read_cr0() ({ \
         unsigned int __dummy; \
         __asm__( \
                 "movl %%cr0,%0\n\t" \
                 :"=r" (__dummy)); \
         __dummy; \
})
这种形式看起来比较陌生,这是因为这不是标准C所定义的形式,而是gcc对C语言的扩充。其中__dummy为C函数所定义的变量;关键词__asm__表示汇编代码的开始。括弧中第一个引号中为汇编指令movl,紧接着有一个冒号,这种形式阅读起来比较复杂。
一般而言,嵌入式汇编语言片段比单纯的汇编语言代码要复杂得多,因为这里存在怎样分配和使用寄存器,以及把C代码中的变量应该存放在哪个寄存器中。为了达到这个目的,就必须对一般的C语言进行扩充,增加对编译器的指导作用,因此,嵌入式汇编看起来晦涩而难以读懂。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:11:22 | 显示全部楼层
catch 发表于 2014-1-18 21:10
在Linux的源代码中,有很多C语言的函数中嵌入一段汇编语言程序段,这就是gcc提供的“asm”功能,例如在in ...

1. 嵌入式汇编的一般形式:
__asm__ __volatile__ ("<asm routine>" : output : input : modify);
   其中,__asm__表示汇编代码的开始,其后可以跟__volatile__(这是可选项),其含义是避免“asm”指令被删除、移动或组合;然后就是小括弧,括弧中的内容是我们介绍的重点:
·      "<asm routine>"为汇编指令部分,例如,"movl %%cr0,%0\n\t"。数字前加前缀“%“,如%1,%2等表示使用寄存器的样板操作数。可以使用的操作数总数取决于具体CPU中通用寄存器的数量,如Intel可以有8个。指令中有几个操作数,就说明有几个变量需要与寄存器结合,由gcc在编译时根据后面输出部分和输入部分的约束条件进行相应的处理。由于这些样板操作数的前缀使用了”%“,因此,在用到具体的寄存器时就在前面加两个“%”,如%%cr0。
·      输出部分(output),用以规定对输出变量(目标操作数)如何与寄存器结合的约束(constraint),输出部分可以有多个约束,互相以逗号分开。每个约束以“=”开头,接着用一个字母来表示操作数的类型,然后是关于变量结合的约束。例如,上例中:
:"=r" (__dummy)
“=r”表示相应的目标操作数(指令部分的%0)可以使用任何一个通用寄存器,并且变量__dummy 存放在这个寄存器中,但如果是:
:“=m”(__dummy)
“=m”就表示相应的目标操作数是存放在内存单元__dummy中。
表示约束条件的字母很多,表 2-5 给出几个主要的约束字母及其含义:
   表2.5  主要的约束字母及其含义
      字母
含义
   m, v,o
表示内存单元
   R
表示任何通用寄存器
   Q
表示寄存器eax, ebx, ecx,edx之一
   I, h
表示直接操作数
   E, F
表示浮点数
   G
表示“任意”
   a, b.c d
表示要求使用寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
   S, D
表示要求使用寄存器esi或edi
   I
表示常数(0~31)
·      输入部分(Input):输入部分与输出部分相似,但没有“=”。如果输入部分一个操作数所要求使用的寄存器,与前面输出部分某个约束所要求的是同一个寄存器,那就把对应操作数的编号(如“1”,“2”等)放在约束条件中,在后面的例子中,我们会看到这种情况。
·      修改部分(modify):这部分常常以“memory”为约束条件,以表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变。
注意,指令部分为必选项,而输入部分、输出部分及修改部分为可选项,当输入部分存在,而输出部分不存在时,分号“:“要保留,当“memory”存在时,三个分号都要保留,例如system.h中的宏定义__cli():
   #define __cli()                 __asm__ __volatile__("cli": : :"memory")

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2014-1-18 21:13:05 | 显示全部楼层
catch 发表于 2014-1-18 21:11
1. 嵌入式汇编的一般形式: __asm__ __volatile__ ("" : output : input : modify);    其中,__asm__表示 ...

2.  Linux源代码中嵌入式汇编举例
   Linux源代码中,在arch目录下的.h和.c文件中,很多文件都涉及嵌入式汇编,下面以system.h中的C函数为例,说明嵌入式汇编的应用。
(1)简单应用
#define __save_flags(x)         __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x)      __asm__ __volatile__("pushl %0 ; popfl": /* no output */
:"g" (x):"memory", "cc")
第一个宏是保存标志寄存器的值,第二个宏是恢复标志寄存器的值。第一个宏中的pushfl指令是把标志寄存器的值压栈。而popl是把栈顶的值(刚压入栈的flags)弹出到x变量中,这个变量可以存放在一个寄存器或内存中。这样,你可以很容易地读懂第二个宏。
(2) 较复杂应用
static inline unsigned long get_limit(unsigned long segment)
{
         unsigned long __limit;
         __asm__("lsll %1,%0"
                 :"=r" (__limit):"r" (segment));
        return __limit+1;
}
这是一个设置段界限的函数,汇编代码段中的输出参数为__limit(即%0),输入参数为segment(即%1)。Lsll是加载段界限的指令,即把segment段描述符中的段界限字段装入某个寄存器(这个寄存器与__limit结合),函数返回__limit加1,即段长。
(3)复杂应用
    在Linux内核代码中,有关字符串操作的函数都是通过嵌入式汇编完成的,因为内核及用户程序对字符串函数的调用非常频繁,因此,用汇编代码实现主要是为了提高效率(当然是以牺牲可读性和可维护性为代价的)。在此,我们仅列举一个字符串比较函数strcmp,其代码在arch/i386/string.h中。
static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
         "1:\tlodsb\n\t"
         "scasb\n\t"
         "jne 2f\n\t"
         "testb %%al,%%al\n\t"
         "jne 1b\n\t"
         "xorl %%eax,%%eax\n\t"
         "jmp 3f\n"
         "2:\tsbbl %%eax,%%eax\n\t"
         "orb $1,%%al\n"
         "3:"
         :"=a" (__res), "=&S" (d0), "=&D" (d1)
                      :"1" (cs),"2" (ct));
return __res;
}
其中的“\n”是换行符,“\t”是tab符,在每条命令的结束加这两个符号,是为了让gcc把嵌入式汇编代码翻译成一般的汇编代码时能够保证换行和留有一定的空格。例如,上面的嵌入式汇编会被翻译成:
1:   lodsb        //装入串操作数,即从[esi]传送到al寄存器,然后esi指向串中下一个元素
      scasb          //扫描串操作数,即从al中减去es:[edi],不保留结果,只改变标志
      jne2f          //如果两个字符不相等,则转到标号2   
      testb %al  %al  
      jne 1b
      xorl %eax %eax
      jmp 3f
2:    sbbl %eax %eax
      orb $1 %al
3:
这段代码看起来非常熟悉,读起来也不困难。其中1f 表示往前(forword)找到第一个标号为1的那一行,相应地,1b表示往后找。其中嵌入式汇编代码中输出和输入部分的结合情况为:
·     返回值__res,放在al寄存器中,与%0相结合;
·     局部变量d0,与%1相结合,也与输入部分的cs参数相对应,也存放在寄存器ESI中,即ESI中存放源字符串的起始地址。
·     局部变量d1, 与%2相结合,也与输入部分的ct参数相对应,也存放在寄存器EDI中,即EDI中存放目的字符串的起始地址。
通过对这段代码的分析我们应当体会到,万变不利其本,嵌入式汇编与一般汇编的区别仅仅是形式,本质依然不变。因此,全面掌握Intel 386 汇编指令乃突破阅读低层代码之根本。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

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

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-5-25 10:24

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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