momo zone

调核人的blog

Monthly Archives: 十月 2009

块设备驱动hd.c 问题1.

static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
        unsigned int head,unsigned int cyl,unsigned int cmd,
        void (*intr_addr)(void))
{
    register int port asm("dx");

    if (drive>1 || head>15)
        panic("Trying to write bad sector");
    if (!controller_ready())
        panic("HD controller not ready");
    do_hd = intr_addr;
    outb_p(hd_info[drive].ctl,HD_CMD);
    port=HD_DATA;
    outb_p(hd_info[drive].wpcom>>2,++port);
    outb_p(nsect,++port);
    outb_p(sect,++port);
    outb_p(cyl,++port);
    outb_p(cyl>>8,++port);
    outb_p(0xA0|(drive<<4)|head,++port);
    outb(cmd,++port);
}

do_hd 的声明找不到啊 ,用编辑器也找不到,难道编译的时候不会报错吗 ? 最终发现他是在blk.h中用宏间接声明的:

#define DEVICE_INTR do_hd
……………………………………………….
#ifdef DEVICE_INTR
void (*DEVICE_INTR)(void) = NULL;
#endif
……………………………………………….

看清楚了吗 ,使了一个障眼法 ,把宏都替换掉就是声明了。因为编译器的预处理会在真正编译前,所以当然不会报错了。

正在计划炮制一个大的图纸…内容是秘密

正在炮制一个大的系统框架图,尺寸预计超过8000X6000 。搞出来后应该可以给很多人以启示了,方便新手学习。
具体内容现在保密,完成时间不确定。

source navigator 不能运行 ?

source navigator 不能运行 ? 好办 ,这个确定是tk.tcl的问题,follow that:

  • comment out lines 182-184 in the file /opt/sourcenav/share/tk8.3/listbox.tcl
  • comment out lines 457-459 in the file /opt/sourcenav/share/tk8.3/text.tcl

应该 OK 了

最近比较烦 TC1 -> TCQ -> TCP

TC1 -> TCQ -> TCP 最近这个搞死人,系统啊,还是原配的好啊 !不要节外生枝再搞一套WEB的 ,除非考虑到完善的维护方面 ,本人对此敬而远之 。

关于ELF 的汇编框架

1. 编译环境

   OS: Solaris 9 X86
   Compiler: gcc 3.3.2
   Linker: Solaris Link Editors 5.x
   Debug Tool: mdb
   Editor: vi

   注:关于编译环境的安装和设置,可以参考文章:Solaris 上的开发环境安装及设置。
       mdb是Solaris提供的kernel debug工具,这里用它做反汇编和汇编语言调试工具。
       如果在Linux平台可以用gdb进行反汇编和调试。

2. 最简C代码分析

    为简化问题,来分析一下最简的c代码生成的汇编代码:
    # vi test1.c
      
    int main()
    {
        return 0;
    }   
   
    编译该程序,产生二进制文件:
    # gcc test1.c -o test1
    # file test1  
    test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped

    test1是一个ELF格式32位小端(Little Endian)的可执行文件,动态链接并且符号表没有去除。
    这正是Unix/Linux平台典型的可执行文件格式。
    用mdb反汇编可以观察生成的汇编代码:

    # mdb test1
    Loading modules: [ libc.so.1 ]
    >; main::dis                       ; 反汇编main函数,mdb的命令一般格式为  <地址>;::dis
    main:          pushl   %ebp       ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
    main+1:        movl    %esp,%ebp  ; esp值赋给ebp,设置main函数的栈基址
    main+3:          subl    $8,%esp
    main+6:          andl    $0xf0,%esp
    main+9:          movl    $0,%eax
    main+0xe:        subl    %eax,%esp
    main+0x10:     movl    $0,%eax    ; 设置函数返回值0
    main+0x15:     leave              ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
    main+0x16:     ret                ; main函数返回,回到上级调用
    >;

    注:这里得到的汇编语言语法格式与Intel的手册有很大不同,Unix/Linux采用AT&T汇编格式作为汇编语言的语法格式
         如果想了解AT&T汇编可以参考文章:Linux AT&T 汇编语言开发指南

    问题:谁调用了 main函数?
     
     在C语言的层面来看,main函数是一个程序的起始入口点,而实际上,ELF可执行文件的入口点并不是main而是_start。
     mdb也可以反汇编_start:
      
    >; _start::dis                       ;从_start 的地址开始反汇编
    _start:              pushl   $0
    _start+2:            pushl   $0
    _start+4:            movl    %esp,%ebp
    _start+6:            pushl   %edx
    _start+7:            movl    $0x80504b0,%eax
    _start+0xc:          testl   %eax,%eax
    _start+0xe:          je      +0xf            <_start+0x1d>;
    _start+0x10:         pushl   $0x80504b0
    _start+0x15:         call    -0x75           <atexit>;
    _start+0x1a:         addl    $4,%esp
    _start+0x1d:         movl    $0x8060710,%eax
    _start+0x22:         testl   %eax,%eax
    _start+0x24:         je      +7              <_start+0x2b>;
    _start+0x26:         call    -0x86           <atexit>;
    _start+0x2b:         pushl   $0x80506cd
    _start+0x30:         call    -0x90           <atexit>;
    _start+0x35:         movl    +8(%ebp),%eax
    _start+0x38:         leal    +0x10(%ebp,%eax,4),%edx
    _start+0x3c:         movl    %edx,0x8060804
    _start+0x42:         andl    $0xf0,%esp
    _start+0x45:         subl    $4,%esp
    _start+0x48:         pushl   %edx
    _start+0x49:         leal    +0xc(%ebp),%edx
    _start+0x4c:         pushl   %edx
    _start+0x4d:         pushl   %eax
    _start+0x4e:         call    +0x152          <_init>;
    _start+0x53:         call    -0xa3           <__fpstart>;
    _start+0x58:        call    +0xfb        <main>;              ;在这里调用了main函数
    _start+0x5d:         addl    $0xc,%esp
    _start+0x60:         pushl   %eax
    _start+0x61:         call    -0xa1           <exit>;
    _start+0x66:         pushl   $0
    _start+0x68:         movl    $1,%eax
    _start+0x6d:         lcall   $7,$0
    _start+0x74:         hlt
    >;

    问题:为什么用EAX寄存器保存函数返回值?
    实际上IA32并没有规定用哪个寄存器来保存返回值。但如果反汇编Solaris/Linux的二进制文件,就会发现,都用EAX保存函数返回值。
    这不是偶然现象,是操作系统的ABI(Application Binary Interface)来决定的。
    Solaris/Linux操作系统的ABI就是Sytem V ABI。

    概念:SFP (Stack Frame Pointer) 栈框架指针

    正确理解SFP必须了解:
        IA32 的栈的概念
        CPU 中32位寄存器ESP/EBP的作用
        PUSH/POP 指令是如何影响栈的
        CALL/RET/LEAVE 等指令是如何影响栈的

    如我们所知:
    1)IA32的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。
    2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
    3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。
    4) POP一个long型数据,过程与PUSH相反,依次将ESP-4、ESP-3、ESP-2、ESP-1从栈内弹出,放入一个32位寄存器。
    5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
    6) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行
    7) ENTER是建立当前函数的栈框架,即相当于以下两条指令:
        pushl   %ebp
        movl    %esp,%ebp
    LEAVE是释放当前函数或者过程的栈框架,即相当于以下两条指令:
        movl ebp esp
        popl  ebp

    如果反汇编一个函数,很多时候会在函数进入和返回处,发现有类似如下形式的汇编语句:
        
        pushl   %ebp            ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
        movl    %esp,%ebp       ; esp值赋给ebp,设置 main函数的栈基址
        ………..             ; 以上两条指令相当于 enter 0,0
        ………..
        leave                   ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
        ret                     ; main函数返回,回到上级调用

    这些语句就是用来创建和释放一个函数或者过程的栈框架的。
    原来编译器会自动在函数入口和出口处插入创建和释放栈框架的语句。
    函数被调用时:
    1) EIP/EBP成为新函数栈的边界
    函数被调用时,返回时的EIP首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP一道行成新函数栈框架的边界
    2) EBP成为栈框架指针SFP,用来指示新函数栈的边界
    栈框架建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace功能的
    3) ESP总是作为栈指针指向栈顶,用来分配栈空间
    栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,例如,分配一个整型数据就是 ESP-4
    4) 函数的参数传递和局部变量访问可以通过SFP即EBP来实现
    由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
        +8+xx(%ebp)         ; 函数入口参数的的访问
        -xx(%ebp)           ; 函数局部变量访问
            
    假如函数A调用函数B,函数B调用函数C ,则函数栈框架及调用关系如下图所示:

下图有点乱,因此删去部分内容,要看原图可参考我的blog
+—————————-+—->; 高地址
| EIP (上级函数返回地址)  |           
+—————————-+  
| EBP (上级函数的EBP)     |
+—————————-+
| Local Variables            |          
| ……….                          |
+—————————–+         
| Arg n(函数B的第n个参数) |  
+—————————–+
| Arg .(函数B的第.个参数)   |
+—————————–+
| Arg 1(函数B的第1个参数) |
+—————————–+  
| Arg 0(函数B的第0个参数) |
+—————————–+
EIP (A函数的返回地址)      |
+—————————–+
| EBP (A函数的EBP)          |
+—————————–+  
| Local Variables             |  
| ……….                           |   
+—————————–+
| Arg n(函数C的第n个参数) |  
+—————————–+   
| Arg .(函数C的第.个参数)   |  
+—————————–+  
| Arg 1(函数C的第1个参数) |
+—————————–+  
| Arg 0(函数C的第0个参数) |  
+—————————–+   
| EIP (B函数的返回地址)     |
+—————————–+
| EBP (B函数的EBP)          |
+—————————–+
| Local Variables             |
| ……….                           |
+—————————–+—>; 低地址

   图 1-1
      
    再分析test1反汇编结果中剩余部分语句的含义:
        
    # mdb test1
    Loading modules: [ libc.so.1 ]
    >; main::dis                        ; 反汇编main函数
    main:          pushl   %ebp                           
    main+1:        movl    %esp,%ebp        ; 创建Stack Frame(栈框架)
    main+3:       subl    $8,%esp       ; 通过ESP-8来分配8字节堆栈空间
    main+6:       andl    $0xf0,%esp    ; 使栈地址16字节对齐
    main+9:       movl    $0,%eax       ; 无意义
    main+0xe:     subl    %eax,%esp     ; 无意义
    main+0x10:     movl    $0,%eax          ; 设置main函数返回值
    main+0x15:     leave                    ; 撤销Stack Frame(栈框架)
    main+0x16:     ret                      ; main 函数返回
    >;

    以下两句似乎是没有意义的,果真是这样吗?
        movl    $0,%eax
        subl     %eax,%esp
      
    用gcc的O2级优化来重新编译test1.c:
    # gcc -O2 test1.c -o test1
    # mdb test1
    >; main::dis
    main:         pushl   %ebp
    main+1:       movl    %esp,%ebp
    main+3:       subl    $8,%esp
    main+6:       andl    $0xf0,%esp
    main+9:       xorl    %eax,%eax      ; 设置main返回值,使用xorl异或指令来使eax为0
    main+0xb:     leave
    main+0xc:     ret
    >;
    新的反汇编结果比最初的结果要简洁一些,果然之前被认为无用的语句被优化掉了,进一步验证了之前的猜测。
    提示:编译器产生的某些语句可能在程序实际语义上没有用处,可以用优化选项去掉这些语句。

    问题:为什么用xorl来设置eax的值?
    注意到优化后的代码中,eax返回值的设置由 movl $0,%eax 变为 xorl %eax,%eax ,这是因为IA32指令中,xorl比movl有更高的运行速度。

    概念:Stack aligned 栈对齐
    那么,以下语句到底是和作用呢?
        subl    $8,%esp
       andl    $0xf0,%esp     ; 通过andl使低4位为0,保证栈地址16字节对齐
      
    表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?
    原来,IA32 系列CPU的一些指令分别在4、8、16字节对齐时会有更快的运行速度,因此gcc编译器为提高生成代码在IA32上的运行速度,默认对产生的代码进行16字节对齐

        andl $0xf0,%esp 的意义很明显,那么 subl $8,%esp 呢,是必须的吗?
    这里假设在进入main函数之前,栈是16字节对齐的话,那么,进入main函数后,EIP和EBP被压入堆栈后,栈地址最末4位二进制位必定是 1000,esp -8则恰好使后4位地址二进制位为0000。看来,这也是为保证栈16字节对齐的。

    如果查一下gcc的手册,就会发现关于栈对齐的参数设置:
    -mpreferred-stack-boundary=n    ; 希望栈按照2的n次的字节边界对齐, n的取值范围是2-12

    默认情况下,n是等于4的,也就是说,默认情况下,gcc是16字节对齐,以适应IA32大多数指令的要求。

    让我们利用-mpreferred-stack-boundary=2来去除栈对齐指令:
      
    # gcc -mpreferred-stack-boundary=2 test1.c -o test1
      
    >; main::dis
    main:       pushl   %ebp
    main+1:     movl    %esp,%ebp
    main+3:     movl    $0,%eax
    main+8:     leave
    main+9:     ret
    >;

    可以看到,栈对齐指令没有了,因为,IA32的栈本身就是4字节对齐的,不需要用额外指令进行对齐。
    那么,栈框架指针SFP是不是必须的呢?
    # gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test
    >; main::dis
    main:       movl    $0,%eax
    main+5:     ret
    >;

    由此可知,-fomit-frame-pointer 可以去除SFP。
      
    问题:去除SFP后有什么缺点呢?
      
    1)增加调式难度
        由于SFP在调试器backtrace的指令中被使用到,因此没有SFP该调试指令就无法使用。
    2)降低汇编代码可读性
        函数参数和局部变量的访问,在没有ebp的情况下,都只能通过+xx(esp)的方式访问,而很难区分两种方式,降低了程序的可读性。
      
    问题:去除SFP有什么优点呢?
      
    1)节省栈空间
    2)减少建立和撤销栈框架的指令后,简化了代码
    3)使ebp空闲出来,使之作为通用寄存器使用,增加通用寄存器的数量
    4)以上3点使得程序运行速度更快

    概念:Calling Convention  调用约定和 ABI (Application Binary Interface) 应用程序二进制接口
         
        函数如何找到它的参数?
        函数如何返回结果?
        函数在哪里存放局部变量?
        那一个硬件寄存器是起始空间?
        那一个硬件寄存器必须预先保留?

    Calling Convention  调用约定对以上问题作出了规定。Calling Convention也是ABI的一部分。
    因此,遵守相同ABI规范的操作系统,使其相互间实现二进制代码的互操作成为了可能。
    例如:由于Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接运行Linux二进制程序的功能。
    详见文章:关注: Solaris 10的10大新变化
            
3. 小结
    本文通过最简的C程序,引入以下概念:
        SFP 栈框架指针
        Stack aligned 栈对齐
        Calling Convention  调用约定 和 ABI (Application Binary Interface) 应用程序二进制接口
    今后,将通过进一步的实验,来深入了解这些概念。通过掌握这些概念,使在汇编级调试程序产生的core dump、掌握C语言高级调试技巧成为了可能。

AT&T 汇编温习

1. Syntax
 
 
 
Register Reference
 
 
 
゚     引用寄存器要在寄存器号前加百分号%,如“movl %eax, %ebx”。
 
゚     80386有如下寄存器:
 
゚     8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
 
゚     8个16-bit寄存器,它们事实上是上面8个32-bit寄存器的低16位:%ax,%bx,
 
%cx,%dx,%di,%si,%bp,%sp;
 
゚     8个8-bit寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它们事实上 是寄存器%ax,%bx,%cx,%dx 的高8位和低8位;
 
゚     6个段寄存器:%cs(code),%ds(data),%ss(stack),%es,%fs,%gs;
 
゚     3个控制寄存器:%cr0,%cr2,%cr3;
 
゚     6个debug寄存器:%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)。

 
 
Operator Sequence
 
 
 
操作数排列是从源(左)到目的(右),如“movl%eax(源),%ebx(目的)”
 
 
ImmediatelyOperator
 
 
 
使用立即数,要在数前面加符号$,  如“movl$0x04,%ebx”
 
或者:
 
 
para=0x04
 
movl$para,%ebx
 
指令执行的结果是将立即数04h装入寄存器ebx。
 
 
Symbol Constant
 
符号常数直接引用如 value:.long0x12a3f2de movlvalue,%ebx
 
指令执行的结果是将常数0x12a3f2de装入寄存器ebx。
 
引用符号地址在符号前加符号$,  如“movl$value,%ebx”则是将符号value的地址装入 寄存器ebx。
 
 
Length of Operator
 
 
 
操作数的长度用加在指令后的符号表示b(byte,  8-bit), w(word, 16-bits), l(long,
 
32-bits),如“movb %al, %bl”,“movw%ax,%bx”,“movl%eax,%ebx”。
 
如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov
 
%ax,%bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于“movw %ax,
 
%bx”。同样道理,指令“mov$4,%ebx”等同于指令“movl$4,%ebx”,“push%al”等同于
“pushb%al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push$4”。

 
 
Sign and Zero Extension
 
 
绝大多数面向80386的AT&T汇编指令与Intel格式的汇编指令都是相同的,符号扩展 指令和零扩展指令则是仅有的不同格式指令。
 
符号扩展指令和零扩展指令需要指定源操作数长度和目的操作数长度,即使在某些指令中这些操作数是隐含的。
 
在AT&T语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz"(对应 Intel语法的movsx和movzx),后面跟上源操作数长度和目的操作数长度。movsbl意味着 movs(from)byte(to)long;movbw意味着movs(from)byte(to)word;movswl 意味着movs  (from)word  (to)long。对于movz指令也一样。比如指令“movsbl %al,
 
%edx”意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。
 
其它的Intel格式的符号扩展指令还有:
 
゚     cbw–sign-extendbytein%altowordin%ax;
 
゚     cwde–sign-extendwordin%axtolongin%eax;
 
゚     cwd–sign-extendwordin%axtolongin%dx:%ax;
 
゚     cdq–sign-extenddwordin%eaxtoquadin%edx:%eax;
 
对应的AT&T语法的指令为cbtw,cwtl,cwtd,cltd。
 
 
Call and Jump
 
 
段内调用和跳转指令为"call","ret"和"jmp",段间调用和跳转指令为"lcall","lret"和
 
"ljmp"。
 
段间调用和跳转指令的格式为“lcall/ljmp$SECTION,$OFFSET”,而段间返回指令则 为“lret$STACK-ADJUST”。
 
 
Prefix
 
 
 
操作码前缀被用在下列的情况:
゚     字符串重复操作指令(rep,repne); ゚     指定被操作的段(cs,ds,ss,es,fs,gs); ゚      进行总线加锁(lock);
 
゚     指定地址和操作的大小(data16,addr16);

在AT&T汇编语法中,操作码前缀通常被单独放在一行,后面不跟任何操作数。例如, 对于重复scas指令,其写法为:
 
repne
 
  scas
 
上述操作码前缀的意义和用法如下:
 
゚     指定被操作的段前缀为cs,ds,ss,es,fs,和gs。在AT&T  语法中,只需要按照 section:memory-operand 的格式就指定了相应的段前缀。比如: lcall%cs:realmode_swtch
 
゚     操作数/地址大小前缀是“data16”和"addr16",它们被用来在32-bit操作数/地址 代码中指定16-bit的操作数/地址。
 
゚     总线加锁前缀“lock”,它是为了在多处理器环境中,保证在当前指令执行期间禁止 一切中断。这个前缀仅仅对ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC, INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG指令有效,如果将Lock前 缀用在其它指令之前,将会引起异常。
 
゚     字符串重复操作前缀"rep","repe","repne"用来让字符串操作重复“%ecx”次。
 
 
Memory Reference
 
 
 
Intel语法的间接内存引用的格式为:
 
section:[base+index*scale+displacement]
 
而在AT&T语法中对应的形式为:
 
 
section:displacement(base,index,scale)
 
其中,base和index是任意的32-bitbase和index寄存器。scale可以取值1,2,4,8。 如果不指定scale值,则默认值为1。section可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。如果你在指令中指定了默认的段前缀,则编译器在目标代 码中不会产生此段前缀代码。
 
 
下面是一些例子:
 
-4(%ebp):base=%ebp,displacement=-4,section没有指定,由于base=%ebp,所 以默认的section=%ss,index,scale没有指定,则index为0。
 
foo(,%eax,4):index=%eax,scale=4,displacement=foo。其它域没有指定。这里默认 的section=%ds。
 
foo(,1):这个表达式引用的是指针foo指向的地址所存放的值。注意这个表达式中没有

 
base和index,并且只有一个逗号,这是一种异常语法,但却合法。
 
%gs:foo:这个表达式引用的是放置于%gs段里变量foo的值。
 
如果call和jump操作在操作数前指定前缀“*”,则表示是一个绝对地址调用/跳转,也 就是说jmp/call指令指定的是一个绝对地址。如果没有指定"*",则操作数是一个相对地址。
 
任何指令如果其操作数是一个内存操作,则指令必须指定它的操作尺寸
 
(byte,word,long),也就是说必须带有指令后缀(b,w,l)。
 
 
2. GCC Inline ASM
 
 
GCC  支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作GCC Inline ASM— — GCC  内联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法 表达的指令直接潜入C/C++代码中,另外也允许我们直接写C/C++代码中使用汇编编写简 洁高效的代码。
 
 
2.1 EssentialInlineASM
 
 
 
GCC中基本的内联汇编非常易懂,我们先来看两个简单的例子:
 
__asm__("movl%esp,%eax"); //看起来很熟悉吧!
 
或者是
 
 
__asm__("
movl$1,%eax   //SYS_exit xor%ebx,%ebx
 
int $0x80
");
 
 
 

 
 
__asm__(
 
"movl$1,%eaxrt"
"xor%ebx,%ebxrt"
 
"int$0x80"
 
);
 
 
 
基本内联汇编的格式是
 
 
__asm____volatile__("InstructionList");

 
 
1.__asm__
 
 
 
__asm__是GCC关键字asm的宏定义:
 
#define__asm__asm
 
__asm__或 asm  用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以 它开头的,是必不可少的。
 
 
 
2.InstructionList
 
 
 
InstructionList是汇编指令序列。它可以是空的,比如:__asm__ __volatile__("");  或
__asm__("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。但并非所 有InstructionList为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory"); 就非常有意义,它向GCC  声明:“我对内存作了改动”,GCC  在编译的时候,会将此因素 考虑进去。
 
我们看一看下面这个例子:
 
 
$catexample1.c
 
int main(int__argc,char*__argv[])
 
{
 
int*__p=(int*)__argc;
(*__p)=9999;
 
 
//__asm__("":::"memory");
 
 
if((*__p)==9999)
return5;
 
 
return(*__p);
 
}
 
 
在这段代码中,那条内联汇编是被注释掉的。在这条内联汇编之前,内存指针__p所指 向的内存被赋值为9999,随即在内联汇编之后,一条if语句判断__p所指向的内存与9999 是否相等。很明显,它们是相等的。GCC  在优化编译的时候能够很聪明的发现这一点。我 们使用下面的命令行对其进行编译:
 
$gcc-O-Sexample1.c
 
 
选项-O表示优化编译,我们还可以指定优化等级,比如-O2表示优化等级为2;选项-S
 
表示将 C/C++源文件编译为汇编文件,文件名和 C/C++文件一样,只不过扩展名由.c  变

 
为.s。
 
我们来查看一下被放在example1.s中的编译结果,我们这里仅仅列出了使用gcc 2.96 在redhat7.3上编译后的相关函数部分汇编代码。为了保持清晰性,无关的其它代码未被列 出。
$catexample1.s main:
pushl %ebp
 
movl   %esp,%ebp
movl   8(%ebp),%eax   #int*__p=(int*)__argc movl    $9999,(%eax)       # (*__p) = 9999
 
movl   $5,%eax        #return5
popl   %ebp ret
 
 
参照一下C源码和编译出的汇编代码,我们会发现汇编代码中,没有if语句相关的代 码,而是在赋值语句(*__p)=9999后直接return5;这是因为GCC认为在(*__p)被赋值之后, 在if语句之前没有任何改变(*__p)内容的操作,所以那条if语句的判断条件(*__p)==9999 肯定是为true  的,所以GCC就不再生成相关代码,而是直接根据为true的条件生成return
 
5的汇编代码(GCC使用eax作为保存返回值的寄存器)。
 
我们现在将example1.c中内联汇编的注释去掉,重新编译,然后看一下相关的编译结 果。
 
$gcc-O-Sexample1.c
$catexample1.s main:
 
pushl %ebp
 
movl   %esp,%ebp
movl   8(%ebp),%eax   #int*__p=(int*)__argc movl $9999,(%eax) # (*__p) = 9999
 
#APP
 
#__asm__("":::"memory")
 
#NO_APP
cmpl   $9999,(%eax)    #(*__p)==9999
 
jne    .L3              #false
 
movl   $5,%eax        #true,return5
 
jmp    .L2
 
.L3:
movl   (%eax),%eax
 
.L2:
popl   %ebp ret

 
由于内联汇编语句__asm__("":::"memory")向 GCC  声明,在此内联汇编语句出现的位 置内存内容可能了改变,所以GCC  在编译时就不能像刚才那样处理。这次,GCC  老老实 实的将if语句生成了汇编代码。
 
可能有人会质疑:为什么要使用__asm__("":::"memory")向GCC声明内存发生了变化? 明明“InstructionList”是空的,没有任何对内存的操作,这样做只会增加GCC生成汇编代 码的数量。
 
确实,那条内联汇编语句没有对内存作任何操作,事实上它确实什么都没有做。但影响 内存内容的不仅仅是你当前正在运行的程序。比如,如果你现在正在操作的内存是一块内存 映射,映射的内容是外围I/O设备寄存器。那么操作这块内存的就不仅仅是当前的程序,I/O 设备也会去操作这块内存。既然两者都会去操作同一块内存,那么任何一方在任何时候都不 能对这块内存的内容想当然。所以当你使用高级语言C/C++写这类程序的时候,你必须让 编译器也能够明白这一点,毕竟高级语言最终要被编译为汇编代码。
 
你可能已经注意到了,这次输出的汇编结果中,有两个符号:#APP和#NO_APP,GCC
 
将内联汇编语句中"Instruction List"所列出的指令放在#APP  和#NO_APP  之间,由于
__asm__("":::"memory")中“InstructionList”为空,所以#APP和#NO_APP中间也没有任何 内容。但我们以后的例子会更加清楚的表现这一点。
 
关于为什么内联汇编__asm__("":::"memory")是一条声明内存改变的语句,我们后面会 详细讨论。
 
刚才我们花了大量的内容来讨论"InstructionList"为空是的情况,但在实际的编程中,
 
"InstructionList"绝大多数情况下都不是空的。它可以有1条或任意多条汇编指令。
 
当在"InstructionList"中有多条指令的时候,你可以在一对引号中列出全部指令,也可 以将一条或几条指令放在一对引号中,所有指令放在多对引号中。如果是前者,你可以将每 一条指令放在一行,如果要将多条指令放在一行,则必须用分号(;)或换行符(n,大多 数情况下n后还要跟一个t,其中n是为了换行,t是为了空出一个tab宽度的空格)将 它们分开。下面的例子都是合法的写法。
 
__asm__("movl %eax, %ebx sti
popl%edi
 
subl%ecx,%ebx");
 
 
__asm__("movl%eax,%ebx;sti popl%edi;subl%ecx,%ebx");
 
 
__asm__("movl%eax,%ebx;stintpopl%edi subl%ecx,%ebx");

 
如果你将指令放在多对引号中,则除了最后一对引号之外,前面的所有引号里的最后一 条指令之后都要有一个分号(;)或(n)或(nt)。比如:
 
 
__asm__("movl %eax, %ebx stin"
 
"popl%edi;"
 
"subl%ecx,%ebx");
 
 
__asm__("movl%eax,%ebx;stint"
"popl%edi;subl%ecx,%ebx");
 
 
__asm__("movl%eax,%ebx;stintpopl%edin"
 
"subl%ecx,%ebx");
 
 
__asm__("movl%eax,%ebx;stintpopl%edi;"
 
"subl%ecx,%ebx");
 
 
 
上述原则可以归结为:
 
゚     任意两个指令间要么被分号(;)分开,要么被放在两行;
 
゚     放在两行的方法既可以从通过n的方法来实现,也可以真正的放在两行;
 
゚     可以使用1对或多对引号,每1对引号里可以放任一多条指令,所有的指令都要被放到引号中。
 
在基本内联汇编中,“InstructionList”的书写的格式和你直接在汇编文件中写非内联汇 编没有什么不同,你可以在其中定义Label,定义对齐(.alignn),定义段(.section name )。 例如:
 
 
__asm__(".align2nt"
 
"movl%eax,%ebxnt"
 
"test%ebx,%ecxnt"
 
"jneerrornt"
"stint"
 
"error:popl%edint"
 
"subl%ecx,%ebx");
 
 
上面例子的格式是 Linux  内联代码常用的格式,非常整齐。也建议大家都使用这种格 式来写内联汇编代码。

 
 
3.__volatile__
 
 
 
__volatile__是GCC关键字volatile的宏定义:
 
#define__volatile__volatile
 
__volatile__或 volatile 是可选的,你可以用它也可以不用它。如果你用了它,则是向 GCC声明“不要动我所写的InstructionList,我需要原封不动的保留每一条指令”,否则当 你使用了优化选项(-O)进行编译时,GCC  将会根据自己的判断决定是否将这个内联汇编表 达式中的指令优化掉。
 
那么GCC  判断的原则是什么?我不知道(如果有哪位朋友清楚的话,请告诉我)。我 试验了一下,发现一条内联汇编语句如果是基本内联汇编的话(即只有“InstructionList”, 没有 Input/Output/Clobber 的内联汇编,我们后面将会讨论这一点),无论你是否使用
 
__volatile__来修饰,GCC 2.96  在优化编译时,都会原封不动的保留内联汇编中的
“InstructionList”。但或许我的试验的例子并不充分,所以这一点并不能够得到保证。 为了保险起见,如果你不想让GCC的优化影响你的内联汇编代码,你最好在前面都加上__volatile__,而不要依赖于编译器的原则,因为即使你非常了解当前编译器的优化原则,
 
你也无法保证这种原则将来不会发生变化。而__volatile__的含义却是恒定的。
 
 
2.2 Inline ASM with C/C++ Expression
 
 
GCC  允许你通过 C/C++表达式指定内联汇编中"Instrcuction List"中指令的输入和输 出,你甚至可以不关心到底使用哪个寄存器被使用,完全靠GCC来安排和指定。这一点可以让程序员避免去考虑有限的寄存器的使用,也可以提高目标代码的效率。先来看几个例子:
 
 
__asm__("":::"memory"); //前面提到的
 
 
__asm__("mov%%eax,%%ebx"
:"=b"(rv)
 
:"a"(foo)
 
:"eax","ebx");
 
 
__asm____volatile__("lidt%0"
:"=m"(idt_descr));
 
 
__asm__("subl%2,%0nt"
 
"sbbl%3,%1"
 
:"=a"(endlow),"=d"(endhigh)
: "g" (startlow), "g" (starthigh),
 
"0"(endlow),"1"(endhigh));

 
 
怎么样,有点印象了吧,是不是也有点晕?没关系,下面讨论完之后你就不会再晕了。
 
(当然,也有可能更晕☺)。讨论开始——
 
带有C/C++表达式的内联汇编格式为:
 
 
__asm__ __volatile__("InstructionList"
 
:Output
 
:Input
 
:Clobber/Modify);
 
 
从中我们可以看出它和基本内联汇编的不同之处在于:它多了3个部分(Input,Output,
 
Clobber/Modify)。在括号中的4个部分通过冒号(:)分开。
 
这4个部分都不是必须的,任何一个部分都可以为空,其规则为:
 
゚     如果Clobber/Modify  为空,则其前面的冒号(:)必须省略。比如__asm__("mov
 
%%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的写法;而__asm__("mov
 
%%eax,%%ebx":"=b"(foo):"a"(inp))则是正确的。
 
゚     如果InstructionList为空,则Input,Output,Clobber/Modify可以不为空,也 可以为空。比如__asm__("":::"memory");和__asm__(""::);都是合法的写法。
 
゚     如果Output,Input,Clobber/Modify都为空,Output,Input之前的冒号(:)既 可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内联汇编,否则, 仍然是一个带有C/C++表达式的内联汇编,此时"InstructionList"中的寄存器写法 要遵守相关规定,比如寄存器前必须使用两个百分号(%%),而不是像基本汇编格 式一样在寄存器前只使用一个百分号(%)。比如__asm__( " mov %%eax,
 
%%ebx"::);__asm__("mov%%eax,%%ebx":)和__asm__("mov%eax,%ebx")
 
都是正确的写法,而__asm__("mov%eax,%ebx"::);__asm__("mov%eax,
 
%ebx":)和__asm__("mov%%eax,%%ebx")都是错误的写法。
 
゚     如果Input,Clobber/Modify为空,但Output不为空,Input前的冒号(:)既可以 省略,也可以不省略。比如__asm__( " mov %%eax, %%ebx" : "=b"(foo)  : );
 
__asm__("mov%%eax,%%ebx":"=b"(foo))都是正确的。
 
゚     如果后面的部分不为空,而前面的部分为空,则前面的冒号(:)都必须保留,否则无 法说明不为空的部分究竟是第几部分。比如,Clobber/Modify,Output  为空, 而Input不为空,则Clobber/Modify前的冒号必须省略(前面的规则),而Output 前的冒号必须为保留。如果Clobber/Modify不为空,而Input和Output都为空, 则Input和Output 前的冒号都必须保留。比如__asm__("mov%%eax,%%ebx"::
 
"a"(foo))和__asm__("mov%%eax,%%ebx":::"ebx")。

从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有 C/C++表达式格式的,其规则在于在"InstructionList"后是否有冒号(:)的存在,如果没有则 是基本格式的,否则,则是带有C/C++表达式格式的。
 
两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个百分号(%), 这一点和非内联汇编相同;而带有C/C++表达式格式则要求寄存器前必须使用两个百分号
 
(%%),其原因我们会在后面讨论。
 
 
1.Output
 
 
 
Output用来指定当前内联汇编语句的输出。我们看一看这个例子:
 
__asm__("movl%%cr0,%0":"=a"(cr0));
 
这个内联汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,指定了一个输出操 作。我们可以很清楚得看到这个输出操作由两部分组成:括号里的部分(cr0)和引号引住的部 分"=a"。这两部分都是每一个输出操作必不可少的。括号里的部分是一个C/C++表达式, 用来保存内联汇编的一个输出值,其操作就等于C/C++的相等赋值cr0=output_value, 因此,括号中的输出表达式只能是C/C++的左值表达式,也就是说它只能是一个可以合法 的放在C/C++赋值操作中等号(=)左边的表达式。那么右值output_value从何而来呢?
 
答案是引号中的内容,被称作“操作约束”(OperationConstraint),在这个例子中操 作约束为"=a",它包含两个约束:等号(=)和字母a,其中等号(=)说明括号中左值表达式cr0 是一个Write-Only的,只能够被作为当前内联汇编的输入,而不能作为输入。而字母a是 寄存器EAX/AX/AL的简写,说明cr0的值要从eax寄存器中获取,也就是说cr0=eax, 最终这一点被转化成汇编指令就是movl%eax,address_of_cr0。现在你应该清楚了吧,操 作约束中会给出:到底从哪个寄存器传递值给cr0。
 
另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并非如此。因为等号(=)约束说明当前的表达式 是一个Write-Only的,但另外还有一个符号— — 加号(+)用来说明当前表达式是一个
Read-Write的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是Read-Only的。因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+) 都表示可写,只不过加号(+)同时也表示是可读的。所以对于一个输出操作来说,其操作约 束只需要有等号(=)或加号(+)中的任意一个就可以了。
 
二者的区别是:等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则 表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(=)约束还 是加号(+)约束所约束的操作表达式都只能放在Output域中,而不能被用在Input域中。
 
另外,有些文档声明:尽管GCC文档中提供了加号(+)约束,但在实际的编译中通不过;我不知道老版本会怎么样,我在GCC2.96中对加号(+)约束的使用非常正常。

 
我们通过一个例子看一下,在一个输出操作中使用等号(=)约束和加号(+)约束的不同。
 
 
$catexample2.c
 
 
intmain(int__argc,char*__argv[])
 
{
 
intcr0=5;
__asm____volatile__("movl%%cr0,%0"
 
:"=a"(cr0));
 
 
return0;
 
}
 
 
$gcc-Sexample2.c
 
 
$catexample2.s main:
pushl %ebp
movl   %esp, %ebp subl    $4,%esp
 
movl   $5, -4(%ebp)     #cr0=5
 
#APP
movl%cr0,%eax
 
#NO_APP
 
movl   %eax,%eax
movl   %eax,-4(%ebp)   #cr0=%eax movl $0,%eax
leave ret
 
 
这个例子是使用等号(=)约束的情况,变量 cr0  被放在内存-4(%ebp)的位置,所以指令
 
mov %eax,-4(%ebp)即表示将%eax的内容输出到变量cr0中。
 
下面是使用加号(+)约束的情况:
 
 
$catexample3.c
 
intmain(int__argc,char*__argv[])
 
{
 
intcr0=5;
 
__asm____volatile__("movl%%cr0,%0"
:"+a"(cr0));
 
return0;
 
}

 
 
$gcc-Sexample3.c
$catexample3.s main:
 
pushl %ebp
movl   %esp,%ebp subl   $4,%esp
 
movl   $5, -4(%ebp)        #cr0= 5
 
movl    -4(%ebp),%eax      #input(%eax=cr0)
 
#APP
movl   %cr0,%eax
 
#NO_APP
 
movl   %eax,-4(%ebp)      #output(cr0=%eax)
movl   $0,%eax leave
ret
 
从编译的结果可以看出,当使用加号(+)约束的时候,cr0不仅作为输出,还作为输入, 所使用寄存器都是寄存器约束(字母a,表示使用eax寄存器)指定的。关于寄存器约束我们 后面讨论。
 
在Output域中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。 例如:
 
 
$catexample3.c
__asm__(
 
"movl   %%eax, %0nt"
 
"pushl %%ebxnt"
 
"popl    %1nt"
 
"movl   %1,%2"
:"+a"(cr0),"=b"(cr1),"=c"(cr2));
 
 
 
2.Input
 
 
 
Input域的内容用来指定当前内联汇编语句的输入。我们看一看这个例子:
 
 
__asm__("movl %0, %%db7"::"a"(cpu->db7));
 
例中 Input  域的内容为一个表达式"a"[cpu->db7),被称作“输入表达式”,用来表示一 个对当前内联汇编的输入。
 
像输出表达式一样,一个输入表达式也分为两部分:带括号的部分(cpu->db7)和带引号 的部分"a"。这两部分对于一个内联汇编输入表达式来说也是必不可少的。

 
括号中的表达式cpu->db7是一个C/C++语言的表达式,它不必是一个左值表达式, 也就是说它不仅可以是放在C/C++赋值操作左边的表达式,还可以是放在C/C++赋值操作 右边的表达式。所以它可以是一个变量,一个数字,还可以是一个复杂的表达式(比如 a+b/c*d)。比如上例可以改为:
 
__asm__("movl %0, %%db7": : "a" (foo));
 
__asm__("movl %0, %%db7": : "a" (0x1000));
__asm__("movl %0, %%db7"::"a"(va*vb/vc));
引号号中的部分是约束部分,和输出表达式约束不同的是,它不允许指定加号(+)约束 和等号(=)约束,也就是说它只能是默认的Read-Only的。约束中必须指定一个寄存器约束, 例中的字母a表示当前输入变量cpu->db7要通过寄存器eax输入到当前内联汇编中。
 
我们看一个例子:
 
 
$catexample4.c
 
intmain(int__argc,char*__argv[])
 
{
intcr0 =5;
 
__asm____volatile__("movl%0,%%cr0"::"a"(cr0));
 
return0;
 
}
 
$gcc-Sexample4.c
$catexample4.s main:
 
pushl %ebp
movl   %esp,%ebp subl   $4,%esp
movl   $5, -4(%ebp)          #cr0=5
 
movl    -4(%ebp),%eax     #%eax=cr0
 
#APP
 
movl   %eax,%cr0
 
#NO_APP
movl   $0,%eax leave
 
ret
 
我们从编译出的汇编代码可以看到,在"InstructionList"之前,GCC按照我们的输入约 束"a",将变量cr0的内容装入了eax寄存器。
 
 
3.OperationConstraint
 
 
每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint,我 们这里来讨论在80386平台上所可能使用的操作约束。

16                                                                                                     Developing Your OwnUnix-LikeOS on IBM PC
 
 
 
3.1 Register Constraint
 
当你当前的输入或输入需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可 以直接指定一个寄存器的名字,比如:
 
 
__asm____volatile__("movl%0,%%cr0"::"eax"(cr0));
 
也可以指定一个缩写,比如:
 
__asm____volatile__("movl%0,%%cr0"::"a"(cr0));
 
如果你指定一个缩写,比如字母a,则GCC将会根据当前操作表达式中C/C++表达式 的宽度决定使用%eax,还是%ax或%al。比如:
 
 
unsignedshort__shrt;
__asm__("mov%0,%%bx"::"a"(__shrt));
 
 
由于变量__shrt是16-bitshort类型,则编译出来的汇编代码中,会让变量__shrt使用
 
%ex寄存器。编译结果为:
 
 
Movw   -2(%ebp),%ax #%ax=__shrt
#APP
 
movl   %ax,%bx
 
#NO_APP
 
 
无论是Input,还是Output操作表达式约束,都可以使用寄存器约束。
 
下表中列出了常用的寄存器约束的缩写。
 
 
约束       意义
 
r                    表示使用一个通用寄存器,由 GCC  在%eax/%ax/%al,%ebx/%bx/%bl,
 
%ecx/%cx/%cl,%edx/%dx/%dl中选取一个GCC认为合适的。
 
g                   表示使用任意一个寄存器,由GCC在所有的可以使用的寄存器中选取一个
 
GCC认为合适的。
 
q                   表示使用一个通用寄存器,和约束r的意义相同。
 
a                   表示使用%eax/%ax/%al
 
b                   表示使用%ebx/%bx/%bl
 
c                    表示使用%ecx/%cx/%cl
 
d                   表示使用%edx/%dx/%dl
 
D                  表示使用%edi/%di
 
S                   表示使用%esi/%si
 
f                    表示使用浮点寄存器
 
t                    表示使用第一个浮点寄存器
 
u                   表示使用第二个浮点寄存器

 
3.2 Memory Constraint
 
如果一个Input/Output  操作表达式的C/C++表达式表现为一个内存地址,不想借助 于任何寄存器,则可以使用内存约束。比如:
 
 
__asm__("lidt%0":"=m"(__idt_addr));
__asm__("lidt%0"::"m"(__idt_addr));
 
 
我们看一下它们分别被放在一个C源文件中,然后被GCC编译后的结果:
 
 
$catexample5.c
/*本例中,变量__sh被作为一个内存输入*/
 
intmain(int__argc,char*__argv[])
 
{
 
char*__sh=(char*)&__argc;
 
 
__asm____volatile__(
 
"lidt%0"
 
:/*nooutput*/
 
:"m"(__sh)
 
);
 
 
return0;
 
}
 
 
$gcc-Sexample5.c
 
 
$catexample5.s main:
 
pushl %ebp
movl   %esp,%ebp subl   $4,%esp
 
leal    8(%ebp),%eax
 
movl   %eax,-4(%ebp) #sh=(char*)&__argc
 
#APP
 
lidt      -4(%ebp)
#NO_APP
movl   $0,%eax leave
 
ret

 
 
$catexample6.c
/*本例中,变量__sh被作为一个内存输出*/
intmain(int__argc,char*__argv[])
 
{
 
char*__sh=(char*)&__argc;
 
 
__asm____volatile__(
"lidt%0"
 
:"=m"(__sh)
 
);
 
 
return0;
}
 
 
$gcc-Sexample6.c
 
 
$catexample6.s main:
 
pushl %ebp
movl   %esp,%ebp subl $4,%esp
 
leal   8(%ebp),%eax
movl   %eax,-4(%ebp) # sh = (char*) &__argc
 
#APP
 
lidt   -4(%ebp)
 
#NO_APP
movl   $0,%eax leave
 
ret
 
首先,你会注意到,在这两个例子中,变量sh没有借助任何寄存器,而是直接参与了 指令lidt的操作。
 
其次,通过仔细观察,你会发现一个惊人的事实,两个例子编译出来的汇编代码是一样 的!虽然,一个例子中变量sh作为输入,而另一个例子中变量sh作为输出。这是怎么回事?
 
原来,使用内存方式进行输入输出时,由于不借助寄存器,所以GCC不会按照你的声 明对其作任何的输入输出处理。GCC只会直接拿来用,究竟对这个C/C++表达式而言是输 入还是输出,完全依赖与你写在"InstructionList"中的指令对其操作的指令。
 
由于上例中,对其操作的指令为lidt,lidt指令的操作数是一个输入型的操作数,所以 事实上对变量sh的操作是一个输入操作,即使你把它放在Output  域也不会改变这一点。 所以,对此例而言,完全符合语意的写法应该是将sh放在Input域,尽管放在Output域 也会有正确的执行结果。

 
所以,对于内存约束类型的操作表达式而言,放在Input域还是放在Output域,对编 译结果是没有任何影响的,因为本来我们将一个操作表达式放在Input域或放在Output域 是希望GCC能为我们自动通过寄存器将表达式的值输入或输出。既然对于内存约束类型的 操作表达式来说,GCC  不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序 员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。
 
 
约束       意义
 
M                  表示使用系统所支持的任何一种内存方式,不需要借助寄存器。
 
3.3 ImmediatelyNumberConstraint
 
如果一个Input/Output  操作表达式的C/C++表达式是一个数字常数,不想借助于任 何寄存器,则可以使用立即数约束。
 
由于立即数在C/C++中只能作为右值,所以对于使用立即数约束的表达式而言,只能 放在Input域。比如:
 
 
__asm____volatile__("movl%0,%%eax"::"i"(100));
 
立即数约束很简单,也很容易理解,我们在这里就不再赘述。
 
 
约束       意义
 
i                    表示输入表达式是一个立即数(整数),不需要借助任何寄存器。
 
F                   表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器。
 
 
 
 
3.4 Generic Constraint
 
 
约束       输入/输出       意义
g                   I,O                         表示可以使用通用寄存器,内存,立即数等任何一种处理方式。
 
0-9                I                             表示和第n个操作表达式使用相同的寄存器/内存。
 
通用约束g是一个非常灵活的约束,当程序员认为一个C/C++表达式在实际的操作中, 究竟使用寄存器方式,还是使用内存方式或立即数方式并无所谓时,或者程序员想实现一个灵活的模板,让GCC可以根据不同的C/C++表达式生成不同的访问方式时,就可以使用 通用约束g。比如:
 
 
#defineJUST_MOV(foo)   
 
__asm__("movl%0,%%eax"::"g"(foo))
 
 
JUST_MOV(100)和JUST_MOV(var)则会让编译器产生不同的代码。

 
 
 
intmain(int__argc,char*__argv[])
 
{
 
JUST_MOV(100);
 
return0;
}
 
 
编译后生成的代码为:
 
 
main:
pushl %ebp
 
movl   %esp,%ebp
 
#APP
 
movl$100,%eax
 
#NO_APP
movl   $0,%eax popl %ebp
 
ret
 
很明显这是立即数方式。而下一个例子:
 
 
intmain(int__argc,char*__argv[])
 
{
JUST_MOV(__argc);
 
return0;
 
}
 
 
经编译后生成的代码为:
 
 
main:
 
pushl %ebp
 
movl   %esp,%ebp
 
#APP
movl   8(%ebp),%eax
 
#NO_APP
movl   $0,%eax popl   %ebp
 
ret
 
 
这个例子是使用内存方式。
 
一个带有C/C++表达式的内联汇编,其操作表达式被按照被列出的顺序编号,第一个 是0,第2个是1,依次类推,GCC最多允许有10个操作表达式。比如:

 
 
__asm__("popl%0nt"
 
"movl%1,%%esint"
 
"movl%2,%%edint"
 
:"=a"(__out)
:"r"(__in1),"r"(__in2));
 
 
此例中,__out所在的Output操作表达式被编号为0,"r"(__in1)被编号为1,"r"(__in2)
 
被编号为2。
 
再如:
 
 
__asm__("movl%%eax,%%ebx"::"a"(__in1),"b"(__in2));
 
此例中,"a"(__in1)被编号为0,"b"(__in2)被编号为1。
 
如果某个Input操作表达式使用数字0到9中的一个数字(假设为1)作为它的操作约 束,则等于向GCC声明:“我要使用和编号为1的Output操作表达式相同的寄存器(如果 Output操作表达式1使用的是寄存器),或相同的内存地址(如果Output操作表达式1使 用的是内存)”。上面的描述包含两个限定:数字0到数字9作为操作约束只能用在Input 操作表达式中,被指定的操作表达式(比如某个Input操作表达式使用数字1作为约束,那 么被指定的就是编号为1的操作表达式)只能是Output操作表达式。
 
由于GCC规定最多只能有10个Input/Output操作表达式,所以事实上数字9作为操 作约束永远也用不到,因为Output操作表达式排在Input操作表达式的前面,那么如果有一个Input操作表达式指定了数字9作为操作约束的话,那么说明Output操作表达式的数 量已经至少为10个了,那么再加上这个Input  操作表达式,则至少为11个了,以及超出 GCC的限制。
 
5、Modifier Characters(修饰符)
 
等号(=)和加号(+)用于对Output操作表达式的修饰,一个Output操作表达式要么被等 号(=)修饰,要么被加号(+)修饰,二者必居其一。使用等号(=)说明此Output操作表达式是 Write-Only的,使用加号(+)说明此Output操作表达式是Read-Write的。它们必须被放在 约束字符串的第一个字母。比如"a="(foo)是非法的,而"+g"(foo)则是合法的。
 
当使用加号(+)的时候,此Output  表达式等价于使用等号(=)约束加上一个Input  表达 式。比如
 
__asm__("movl%0,%%eax;addl%%eax,%0": "+b"(foo))
 
 
等价于
 
 
__asm__("movl%0,%%eax;addl%%eax,%0":"+b"(foo))
 
但如果使用后一种写法,"InstructionList"中的别名也要相应的改动。关于别名,我们

 
后面会讨论。
 
像等号(=)和加号(+)修饰符一样,符号(&)也只能用于对Output操作表达式的修饰。当 使用它进行修饰时,等于向GCC声明:"GCC不得为任何Input操作表达式分配与此Output 操作表达式相同的寄存器"。其原因是&修饰符意味着被其修饰的Output操作表达式要在所 有的Input操作表达式被输入前输出。我们看下面这个例子:
 
 
intmain(int__argc,char*__argv[])
 
{
 
int__in1=8,__in2=4,__out=3;
 
 
__asm__("popl%0nt"
"movl%1,%%esint"
 
"movl%2,%%edint"
 
:"=a"(__out)
 
:"r"(__in1),"r"(__in2));
 
 
return0;
 
}
 
此例中,%0  对应的就是Output  操作表达式,它被指定的寄存器是%eax,整个 InstructionList的第一条指令popl%0,编译后就成为popl%eax,这时%eax的内容已经 被修改,随后在InstructionList后,GCC会通过movl%eax,address_of_out  这条指令将
%eax的内容放置到Output变量__out中。对于本例中的两个Input操作表达式而言,它们 的寄存器约束为"r",即要求GCC为其指定合适的寄存器,然后在Instruction List之前将
__in1和__in2的内容放入被选出的寄存器中,如果它们中的一个选择了已经被__out  指定的 寄存器%eax,假如是__in1,那么GCC  在Instruction List  之前会插入指令movl address_of_in1,%eax,那么随后popl%eax指令就修改了%eax的值,此时%eax中存放的 已经不是Input变量__in1的值了,那么随后的movl%1,%%esi指令,将不会按照我们的 本意— — 即将__in1的值放入%esi中— — 而是将__out的值放入%esi中了。
 
下面就是本例的编译结果,很明显,GCC为__in2选择了和__out相同的寄存器%eax, 这与我们的初衷不符。
 
main:
 
pushl %ebp
movl   %esp,%ebp subl   $12,%esp movl       $8, -4(%ebp) movl $4, -8(%ebp) movl       $3, -12(%ebp)
movl    -4(%ebp),%edx      #__in1使用寄存器%edx movl -8(%ebp),%eax        #__in2使用寄存器%eax

 
 
#APP
 
popl   %eax
movl   %edx,%esi movl %eax,%edi
 
#NO_APP
 
movl   %eax,%eax
movl   %eax,-12(%ebp)   #__out使用寄存器%eax movl $0,%eax
 
leave
 
为了避免这种情况,我们必须向GCC声明这一点,要求GCC为所有的Input操作表 达式指定别的寄存器,方法就是在Output操作表达式"=a"(__out)的操作约束中加入&约束, 由于GCC规定等号(=)约束必须放在第一个,所以我们写作"=&a"(__out)。
 
下面是我们将&约束加入之后编译的结果:
 
main:
 
pushl %ebp
movl   %esp,%ebp subl $12,%esp movl       $8,-4(%ebp) movl   $4,-8(%ebp) movl   $3,-12(%ebp)
movl   -4(%ebp),%edx    #__in1使用寄存器%edx movl   -8(%ebp),%eax
movl   %eax,%ecx       #__in2使用寄存器%ecx
 
#APP
 
popl   %eax
movl   %edx,%esi movl %ecx,%edi
#NO_APP
 
movl   %eax,%eax
movl   %eax,-12(%ebp)   #__out使用寄存器%eax movl   $0,%eax
leave ret
 
 
 
OK!这下好了,完全与我们的意图吻合。
 
如果一个Output操作表达式的寄存器约束被指定为某个寄存器,只有当至少存在一个 Input  操作表达式的寄存器约束为可选约束时,(可选约束的意思是可以从多个寄存器中选 取一个,或使用非寄存器方式),比如"r"或"g"时,此Output操作表达式使用&修饰才有意 义。如果你为所有的Input操作表达式指定了固定的寄存器,或使用内存/立即数约束,则 此Output操作表达式使用&修饰没有任何意义。比如:

 
 
__asm__("popl%0nt"
 
"movl%1,%%esint"
 
"movl%2,%%edint"
 
:"=&a"(__out)
:"m"(__in1),"c"(__in2));
 
 
此例中的Output操作表达式完全没有必要使用&来修饰,因为__in1和__in2都被指定 了固定的寄存器,或使用了内存方式,GCC无从选择。
 
但如果你已经为某个Output操作表达式指定了&修饰,并指定了某个固定的寄存器, 你就不能再为任何Input操作表达式指定这个寄存器,否则会出现编译错误。比如:
 
 
__asm__("popl%0nt"
 
"movl%1,%%esint"
"movl%2,%%edint"
 
:"=&a"(__out)
 
:"a"(__in1),"c"(__in2));
 
 
本例中,由于__out已经指定了寄存器%eax,同时使用了符号&修饰,则再为__in1指 定寄存器%eax就是非法的。
 
反过来,你也可以为Output  指定可选约束,比如"r","g"等,让GCC为其选择到底使 用哪个寄存器,还是使用内存方式,GCC在选择的时候,会首先排除掉已经被Input操作 表达式使用的所有寄存器,然后在剩下的寄存器中选择,或干脆使用内存方式。比如:
 
 
__asm__("popl%0nt"
 
"movl%1,%%esint"
 
"movl%2,%%edint"
 
:"=&r"(__out)
: "a" (__in1), "c"(__in2));
 
 
 
本例中,由于__out指定了约束"r",即让GCC为其决定使用哪一格寄存器,而寄存器
 
%eax和%ecx已经被__in1和__in2使用,那么GCC在为__out选择的时候,只会在%ebx
 
和%edx中选择。
 
前3个修饰符只能用在Output  操作表达式中,而百分号[%]修饰符恰恰相反,只能用 在Input操作表达式中,用于向GCC声明:“当前Input操作表达式中的C/C++表达式可 以和下一个Input操作表达式中的C/C++表达式互换”。这个修饰符号一般用于符合交换律 运算,比如加(+),乘(*),与(&),或(|)等等。我们看一个例子:

 
 
intmain(int__argc,char*__argv[])
 
{
 
int__in1=8,__in2=4,__out=3;
 
 
__asm__("addl%1,%0nt"
 
:"=r"(__out)
 
:"%r"(__in1),"0"(__in2));
 
 
return0;
}
 
 
在此例中,由于指令是一个加法运算,相当于等式__out=__in1+__in2,而它与等式
__out=__in2+__in1没有什么不同。所以使用百分号修饰,让GCC知道__in1和__in2可 以互换,也就是说GCC可以自动将本例的内联汇编改变为:
 
 
__asm__("addl%1,%0nt"
:"=r"(__out)
 
:"%r"(__in2),"0"(__in1));
 
 
 
下表总结了各种修饰符的意义:
 
 
修饰符     输入/输出      意义
 
=                   O                        表示此Output操作表达式是Write-Only的。
 
+                   O                        表示此Output操作表达式是Read-Write的。
 
&                  O                        表示此Output操作表达式独占为其指定的寄存器。
%                  I                          表示此Input  操作表达式中的C/C++表达式可以和下一 个Input操作表达式中的C/C++表达式互换。
 
 
4.  占位符
 
 
 
什么叫占位符?我们看一看下面这个例子:
 
__asm__("addl%1,%0nt"
 
:"=a"(__out)
 
:"m"(__in1),"a"(__in2));
 
这个例子中的%0和%1就是占位符。每一个占位符对应一个Input/Output操作表达式。 我们在之前已经提到,GCC规定一个内联汇编语句最多可以有10个Input/Output操作表 达式,然后按照它们被列出的顺序依次赋予编号0到9。对于占位符中的数字而言,和这些 编号是对应的。
 
由于占位符前面使用一个百分号(%),为了区别占位符和寄存器,GCC  规定在带有

 
C/C++表达式的内联汇编中,"InstructionList"中直接写出的寄存器前必须使用两个百分号
 
(%%)。
 
GCC对其进行编译的时候,会将每一个占位符替换为对应的Input/Output  操作表达 式所指定的寄存器/内存地址/立即数。比如在上例中,占位符%0对应Output操作表达式
 
"=a"(__out),而"=a"(__out)指定的寄存器为%eax,所以把占位符%0  替换为%eax,占位符
 
%1对应Input  操作表达式"m"(__in1),而"m"(__in1)被指定为内存操作,所以把占位符%1
 
替换为变量__in1的内存地址。
 
也许有人认为,在上面这个例子中,完全可以不使用%0,而是直接写%%eax,就像这 样:
 
__asm__("addl%1,%%eaxnt"
 
: "=a"(__out)
 
:"m"(__in1),"a"(__in2));
 
和上面使用占位符%0  没有什么不同,那么使用占位符%0  就没有什么意义。确实,两 者生成的代码完全相同,但这并不意味着这种情况下占位符没有意义。因为如果不使用占位符,那么当有一天你想把变量__out的寄存器约束由a改为b时,那么你也必须将addl指 令中的%%eax改为%%ebx,也就是说你需要同时修改两个地方,而如果你使用占位符,你 只需要修改一次就够了。另外,如果你不使用占位符,将不利于代码的清晰性。在上例中,如果你使用占位符,那么你一眼就可以得知,addl  指令的第二个操作数内容最终会输出到 变量__out中;否则,如果你不用占位符,而是直接将addl指令的第2个操作数写为%%eax, 那么你需要考虑一下才知道它最终需要输出到变量__out中。这是占位符最粗浅的意义。毕 竟在这种情况下,你完全可以不用。
 
但对于这些情况来说,不用占位符就完全不行了:
 
首先,我们看一看上例中的第1个Input操作表达式"m"(__in1),它被GCC替换之后, 表现为addladdress_of_in1,%%eax,__in1的地址是什么?编译时才知道。所以我们完全 无法直接在指令中去写出__in1的地址,这时使用占位符,交给GCC在编译时进行替代, 就可以解决这个问题。所以这种情况下,我们必须使用占位符。
 
其次,如果上例中的Output  操作表达式"=a"(__out)改为"=r"(__out),那么__out  在究 竟使用那么寄存器只有到编译时才能通过GCC来决定,既然在我们写代码的时候,我们不 知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。
 
 
 
5.Clobber/Modify
 
 
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望 GCC在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify  域声明这些寄存 器或内存。

 
这种情况一般发生在一个寄存器出现在"Instruction List",但却不是由 Input/Output 操作表达式所指定的,也不是在一些 Input/Output  操作表达式使用"r","g"约束时由 GCC 为其选择的,同时此寄存器被"InstructionList"中的指令修改,而这个寄存器只是供当前内 联汇编临时使用的情况。比如:
 
 
__asm__("movl%0,%%ebx"::"a"(__foo):"bx");
 
寄存器%ebx  出现在"Instruction List  中",并且被movl  指令修改,但却未被任何 Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定"bx",以让GCC知 道这一点。
 
因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操 作表达式使用"r","g"约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚 的— — 它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。但 除此之外,GCC  对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以如果你真 的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify中声明它们,让GCC 针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。
 
在Clobber/Modify域中指定这些寄存器的方法很简单,你只需要将寄存器的名字使用双引号("")引起来。如果有多个寄存器需要声明,你需要在任意两个声明之间用逗号隔开。 比如:
 
__asm__("movl%0,%%ebx;popl%%ecx"
 
:/*nooutput*/
 
:"a"(__foo):"bx","cx");
 
这些串包括:
 
 
声明的串                  代表的寄存器
 
"al","ax","eax"                              %eax
 
"bl","bx","ebx"                             %ebx
 
"cl","cx","ecx"                               %ecx
 
"dl","dx","edx"                             %edx
 
"si","esi"                                      %esi
 
"di","edi"                                    %edi
 
由上表可以看出,你只需要使用"ax","bx","cx","dx","si","di"就可以了,因为其它的都和 它们中的一个是等价的。
 
如果你在一个内联汇编语句的Clobber/Modify域向GCC声明某个寄存器内容发生了 改变,GCC  在编译时,如果发现这个被声明的寄存器的内容在此内联汇编语句之后还要继 续使用,那么GCC会首先将此寄存器的内容保存起来,然后在此内联汇编语句的相关生成 代码之后,再将其内容恢复。我们来看两个例子,然后对比一下它们之间的区别。

 
这个例子中声明了寄存器%ebx内容发生了改变:
 
$catexample7.c
 
intmain(int__argc,char*__argv[])
 
{
intin=8;
 
__asm__("addl%0,%%ebx"
 
:/*nooutput*/
 
:"a"(in):"bx");
 
 
return0;
 
}
 
$gcc-O-Sexample7.c
$catexample7.s main:
pushl %ebp
 
movl   %esp,%ebp
pushl %ebx       #%ebx内容被保存
 
movl   $8,%eax
 
#APP
addl   %eax,%ebx
 
#NO_APP
 
movl   $0,%eax
movl   (%esp),%ebx     #%ebx内容被恢复
leave ret
 
下面这个例子的C源码与上一个例子除了没有声明%ebx寄存器发生了改变之外,其它 都相同。
 
 
$catexample8.c
 
intmain(int__argc,char*__argv[])
 
{
 
intin=8;
 
 
__asm__("addl%0,%%ebx"
 
:/*nooutput*/
 
:"a"(in));
 
 
return0;
}

 
 
$ gcc-O-Sexample8.c
$catexample8.s main:
 
pushl %ebp
movl   %esp,%ebp movl   $8,%eax
 
#APP
 
addl%eax,%ebx
 
#NO_APP
movl   $0,%eax popl %ebp
 
ret
 
仔细对比一下example7.s和example8.s,你就会明白在Clobber/Modify域声明一个 寄存器的意义。
 
另外需要注意的是,如果你在Clobber/Modify域声明了一个寄存器,那么这个寄存器 将不能再被用做当前内联汇编语句的Input/Output  操作表达式的寄存器约束,如果 Input/Output  操作表达式的寄存器约束被指定为"r"或"g",GCC也不会选择已经被声明在 Clobber/Modify中的寄存器。比如:
 
__asm__("movl%0,%%ebx"
::"a"(__foo):"ax","bx");
 
 
此例中,由于Output操作表达式"a"(__foo)的寄存器约束已经指定了%eax寄存器,那 么再在Clobber/Modify域中指定"ax"就是非法的。编译时,GCC会给出编译错误。
 
除了寄存器的内容会被改变,内存的内容也可以被修改。如果一个内联汇编语句
"InstructionList"中的指令对内存进行了修改,或者在此内联汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其Output  操作表达式使用"m"约束,这种情况下 你需要使用在Clobber/Modify域使用字符串"memory"向GCC声明:“在这里,内存发生 了,或可能发生了改变”。例如:
 
 
void*memset(void*__s,char__c,size_t__count)
{
 
__asm__("cldnt"
 
"repnt"
 
"stosb"
 
:/*nooutput*/
:"a"(__c),"D"(__s),"c"(__count)
 
:"cx","di","memory");
 
return__s;
 
}

此例实现了标准函数库memset,其内联汇编中的stosb对内存进行了改动,而其被修 改的内存地址s被指定装入%edi,没有任何Output操作表达式使用了"m"约束,以指定内 存地址s处的内容发生了改变。所以在其Clobber/Modify域使用"memory"向GCC声明: 内存内容发生了变动。
 
如果一个内联汇编语句的Clobber/Modify域存在"memory",那么GCC会保证在此内 联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使 用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷 贝。因为这个时候寄存器中的拷贝已经很可能和内存处的内容不一致了。
 
 
这只是使用"memory"时,GCC  会保证做到的一点,但这并不是全部。因为使用
"memory"是向GCC  声明内存发生了变化,而内存发生变化带来的影响并不止这一点。比 如我们在前面讲到的例子:
 
 
intmain(int__argc,char*__argv[])
 
{
 
int* __p = (int*)__argc;
 
(*__p)=9999;
 
__asm__("":::"memory");
if((*__p)==9999)
 
return5;
 
 
return(*__p);
 
}
 
 
本例中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC 在优化时会意识到这一点,而直接只生成return  5的汇编代码,而不会再生成if语句的相 关代码,而不会生成return(*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明 内存变化之外,什么都没有做。但GCC此时就不能简单的认为它不需要判断都知道(*__p) 一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语 句相关代码。
 
当一个内联汇编指令中包含影响eflags寄存器中的条件标志(也就是那些Jxx等跳转指 令要参考的标志位,比如,进位标志,0标志等),那么需要在Clobber/Modify域中使用"cc" 来声明这一点。这些指令包括adc,div,popfl,btr,bts等等,另外,当包含call指令时, 由于你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用"cc"。
 
我很少在相关资料中看到有关"cc"的确切用法,只有一份文档提到了它,但还不是i386 平台的,只是说"cc"是处理器平台相关的,并非所有的平台都支持它,但即使在不支持它的 平台上,使用它也不会造成编译错误。我做了一些实验,但发现使用"cc"和不使用"cc"所生 成的代码没有任何不同。但Linux2.4的相关代码中用到了它。如果谁知道在i386平台上"cc" 的细节,请和我联系。

另外,还可以在Clobber/Modify域指定数字0到9,以声明第n个Input/Output操 作表达式所使用的寄存器发生了变化,但正如我们在前面所提到的,如果你为某个 Input/Output  操作表达式指定了寄存器,或使用"g","r"等约束让 GCC  为其选择寄存器, GCC  已经知道哪个寄存器内容发生了变化,所以这么做没有什么意义;我也作了相关的试 验,没有发现使用它会对GCC  生成的汇编代码有任何影响,至少在i386  平台上是这样。 Linux 2.4的所有i386平台相关内联汇编代码中都没有使用这一点,但S390平台相关代码 中有用到,但由于我对S390汇编没有任何概念,所以,也不知道这么做的意义何在。

常见音源对比

1.密纹唱片

密纹唱片(Long Playing record,简称LP)是一种每分钟33转的乙烯基材料制成的唱片,一般直径为10或12英寸。LP推出于1948年,知道1988年CD被广泛使用前一直是唱片发行的首选,并且在2006年开始再次开始受到大量关注,需求量逐年增大。
密纹唱片使用模拟方式录音,所以对于HIFI来说LP唱片比数字媒体具有先天的优势。早期的LP是单声道的,在1957年双声道LP才开始上市,并且在1970年代出现了嘶声道唱片,不过限于消费者所拥有的器材四声道LP并没有流行起来。除了黑色的LP以外还出现过不同颜色和带有图案的版本。
虽然大多数的LP转速为33转,但是有一些“超保真度”的盘片转速为45甚至是78转的唱片。1972年喝蓝飞利浦公司研制成功镭射唱片(Laser Disc,简称LD),1978年投入市场,这种存储媒体流行于1980~1990年代。光盘直径20~30厘米,光盘空间很大,所以没有使用任何压缩技术,很多人认为LD使用了数码技术,但是其实LD使用的是频率调制(Frequency Modulation)的类比方式存储在影碟上。所以LD的在保真度上与LP并没有很大差别。

LP播放器历史资料:
1877年,爱迪生在替西方电子(Western Electric)研究简单的自动电报中继装置过程中,意外地制造出名为Phonograph的及其,并当年12月发表并申请专利,便是人类第一部留声机。
1888年,以電池驱动马达代替手摇。增加了播放的穩定性。使用扁圆形塗蜡锌版作为播放和录音的媒体 – 唱片。
1919年,电子放大器应用于留声机──贝尔实验室麦克斯菲尔德(J. P. Maxfield)及哈里森(H. C. Harison)成功地制造出电子式留声机。
1948年,制作唱片的材料改用塑料,这是促成密纹唱片、LP得以产生的一项重要因素。
1955年 立体声唱片开始普及。
1981年 镭射唱片(LD)产生,一直到今天的CD普及。

2. 卡式录音带

卡式录音带(Compact Audio Cassette),这个大家应该都很熟悉了,它由飞利浦公司在1963年发明,在1970年代代替了LP成为主流, 1973年日本腌制成功包钴磁粉磁带, 1978年美国生产出金属磁粉带, 1980年代Sony的Walkman开始大范围流行。后来日本日立玛克塞尔公司创造了MCMT技术(特殊定向技术、超微粒子及其分散技术)制成了微型及数码盒式录音带,是音频记录进入数字化时代。不过卡带有几个致命的缺点,就是容易磁化、潮湿、走音、食带、复制副本时损失音质等等

3. CD(Compact Disc)

从LD诞生开始,光盘有了很大的发展。我们所说的CD全称是CD-DA(Compact Disc-Digital Audio,精密数字音频光盘)。1977年,Sony公司把模/数和数/模转换器加到其Betamax录音机上用来录音。不过用户对于录音的质量并不满意,所以索尼开发了降噪技术。1978年九月,Sony对卡拉扬只会的演奏会的一次预言进行录音,随后邀请了卡拉扬对录音质量进行评价。这时飞利浦的技术人员想到了使用光存储技术来记录声音,不过当时的光存储媒介是30cm的盘片,可存储13小时20分钟的声音,这对于唱盘公司来说是难以接受的。飞利浦于1963年发布的卡带取得巨大成功,所以飞利浦高层认为CD的尺寸与卡带比不能过大。卡带对角长为11.5cm,CD的直径是12cm。1980年,两家公司发布了音乐光盘的红皮书(Red book, CD-Audio)。不过光盘的内径采用了荷兰人的建议,1.5cm,是当时荷兰10分币的直径。
CD直径的确定具有一定的传奇色彩,索尼当时的副总裁是交响乐的忠实爱好者,所以索尼坚持新开发的CD应至少应能存下贝多芬的第九交响曲。而贝多芬的第九交响曲又有许多指挥版本,卡拉杨的版本长66分钟,而最长的版本是74分钟。74分钟音乐对应12cm的光盘直径。不过,飞利浦的研发人员对索尼的建议却持怀疑态度。他们认为,这么大的光盘会塞不进西装口袋。为此,索尼的研发人员特地测量了来自世界各地的西装尺寸,发现西装口袋装12cm直径的光盘毫无问题。贝多芬就这样确定了CD光盘的标准。1980年,两家公司发布了音乐光盘的红皮书(Red book, CD-Audio)。不过光盘的内径采用了荷兰人的建议,1.5cm,是当时荷兰10分币的直径。1982年8月17日,在德国汉诺威宝丽金公司的工厂内开始生产第一批音乐光盘。1982年10月1日,第一批CD播放机上市。1988年CD盘的产量为一亿张。

4. CD唱片的发展
随着发烧群体对音质的不懈追求,普通CD唱片已经经过了很多改进:24K金CD、HDCD、XRCD、SACD、DSD CD、LPCD、JSRM CD、DVD-audio等更加高保真的媒体不断出现。最普通的CD唱片的采样频率为44.1kHz,16比特量化(红皮书中规定)。理论上的频响范围是 20Hz-20kHz、动态范围在90dB左右,信噪比一般也不低于90dB。由于普通CD唱片的采样频率过低,量化的比特数也不够高,所以听感上会有很大不足。对于追求音质的发烧友来说,普通CD的音质已经很难满足他们挑剔的耳朵了,市场需要一种音质能得到可观改善,价格也能普遍承受的高级CD品种,在这种情况下,HDCD应运而生。

HDCD
HDCD是英文High Definition Compatible Digital(高解析度CD或者高精度CD)的简称,诞生于90年代初期,当时的美国太平洋音响微音公司(Pacific Microsonics)研发了HDCD技术,并受到了国际音响界的极大关注和重视,美国RR(Reference Recoding)唱片公司率先推出了编号为RR-S3CD的HDCD样片。HDCD与普通CD比较,采用的是18比特的录音方式:16比特为普通全频带数码录音,另外2比特是通过专业设备记录的高频与超高频信号(包含有丰富的相位信息),在随后进行的母盘制作过程中,将全频带部分压缩成为14比特,然后相位专用的2比特单独记录,最终压制成HDCD唱片。
这种独特的制作工艺决定了HDCD必须在有专门解码器的CD机上播放,普通CD机只能读出14比特的全频带音频信号,丢失了很多丰富的信息,动态范围仅能达到78DB。在具有HDCD解码功能的CD机上,由于读出并复合了2比特的相位和高频信号,所以音质清晰细腻、动态范围广、有着极高的信噪比。HDCD 最大的悲哀是“生不逢时”,因为HDCD的播放设备是从20世纪90年代末期和21世纪初才开始大量上市的,此时SACD和DVD- Audio这两种划时代的CD唱片已经先声夺人了,再加上DG、EMI、DECCA等唱片巨头不再看好HDCD,这种唱片的数量现在已经越来越少。

XRCD
XRCD(Extended Resolution Compact Disc 超解析力CD),让人惊奇的是这种发烧靓碟可以用普通CD机播放,而不像HDCD和SACD需要专门的硬件解码。但令人惋惜的是,由于制作费用过于高昂,生产该种唱片的日本JVC公司已经于2006年停止了这种唱片的供应。曾经有人说XRCD是迄今最完美的 16位CD,并预言XRCD是“后CD时代”的“末代皇帝”,看样子是灵验了。不过话说回来,XRCD的停产是正常的,因为它只是普通CD的一种极致延伸,当16比特的录音技术瓶颈被突破后,这种过渡性的产品必将消亡。
XRCD曾经细分出了XRCD2、XRCD24等小类,但是其基本原理都是相同的,这种1997年日本JVC公司开发研制出来的独家技术开创了录音硬件、理论的新境界。JVC为了最大限度的降低时基误差(Jitter),开发了K2接口,包括了Mastering设备、制造工续、硬件与理论等多方面成果。在数字化过程的开始,JVC先把讯号储存在Sony的PCM-9000 MO光盘上,然后进行一连串的K2编码以及K2刻盘、压片,全由JVC原厂进行,透过SDIF-2传输,整个过程中,JVC在时钟位准与电源净化上也下了大功夫,K2所用的20位,128倍超取样A/D转换,动态范围可达108dB,总谐波失真-96dB,有效频宽范围内频率误差小于0.05dB。由于 XRCD的录音处理技术还是在目前的CD标准范围之内,这种追求极致,不惜工本的方法使得XRCD发烧碟的价格都过于昂贵,再加上现在XRCD已经停产,所以它更多意义上成了一种收藏品,留存会越来越少。

SACD
SACD才是真正意义上的革命性发烧CD品种,它还是由以前CD标准的制定者:飞利浦和索尼共同研制(1999年联合发布)。SACD全称是Super Audio Compact Disc,即超级音频压缩光盘,它的采样频率为2.8224MHz,是普通CD采样频率的整整64倍!由于SACD采用的是DSD(Direct Stream Digital)数字音频技术,从头到尾都是1Bit形态,不需任何转换,这与普通CD采用的线性脉冲编码调制PCM(PULSE CODE MODULATION)相比有着极高的保真度。此外SACD采用了PSP PIT SIGNAL PROCESSING,即PIT信号处理技术对版权进行了保护,所以目前还很少见其盗版,这一点也深得各大唱片公司青睐,对SACD的支持力度很大,尤其是古典唱片工业的巨头纷纷发行了新老唱片的SACD版。硬件方面:欧美和日系发烧器材厂都推出了价格不菲的SACD播放机(SONY、马兰士、金嗓子、 Meridian、Krell),这对SACD来说无疑是高速发展的温床。
SACD目前有两种形式,一种是纯粹的SACD,除了专用的SACD播放机之外,和任何一种播放器材都不兼容。还有一种复合盘式的SACD:下层是 SACD信号,上层是普通CD(典型的单面双层式CD),这种复合盘的SACD在普通CD机上就可以播放,但只读上层的信号。SACD的记录格式有两种:一种是双声道格式,另一种是多声道(6声道)格式。不过SACD唱片的价格较高,专用的SACD播放机也很贵,这给它的普及带来了一定的阻碍。

LPCD
BOSS登场
面对铺天盖地的数码音源,老烧们念念不忘的是模拟时代的LP唱片,虽然LP在技术指标及耐用程度上不如CD唱片(主要表现在信噪比差、频响不够宽、动态小、失真大等),但LP唱片那温润甜美的音色、极高的声音密度和极强的空气感确是数码音源一直比较欠缺的,一般认为这种现象是由于CD唱片在制作过程中反复的数模转换造成的,针对这种音质劣化,著名录音师、雨果唱片的创始人:易有伍先生根据自己30多年音乐造诣和20多年的录音经验研发了LPCD:一种能在普通CD载体上得到LP模拟音质特色的发烧CD 品种。
一般的唱片制作都要经过:母带–母盘CDR–玻璃母模–金属母模–压碟–CD这个过程,但雨果以损失最少的方式直接从母带转成CDR,每张 LPCD的制作其实就是直接以制作母盘来做成CDR。LPCD的规格目前有LPCD 33和LPCD 45两种,前者是经特别处理的CD,后者是母带直刻的CDR,据说LPCD33的音质就已经超越了XRCD和HDCD了,LPCD45更是极品靓碟,但用户的CD机必须支持CDR的播放。雨果现在发行的LPCD45系列里面都有二张CD,一张是正常的CD版本,一张是读取深绿层的最高规格CDR版。 LPCD由于对CD边缘经过了精益求精的处理,所以减轻了CD伺服系统的工作压力和信号失真,更近一步的提升了音质。

…….看完这个我笑了

有朋友说SEN就是听的厅堂感,AKG是平淡的韵味,白牙和德国歌德走的是全频解析的路子。喜欢白牙和德国歌德的人说SEN和AKG不够清楚。喜欢SEN说AKG垮淡。喜欢AKG说SEN假回响。喜欢SEN和AKG一起说白牙线条感十足,干涩不丰满,无神韵。

使用vim进行文件比对

 

vimdiff file1 file2

vim -d file1 file2

命令:

dp 将当前文件的当前差异合并到另一个文件中

do 将另一个文件中的对应修改更新到当前文件中

[c 查找前一个差异处

]c 查找后一个差异处

使用diff查看文件差异

diff file1 file2

简单输出模式,只输出差异内容

diff -c file1 file2

diff -C 5 file1 file2

上下文显示输出模式,显示差异区域上下n行的文本,两个文件的差异块是分开显示的

diff -u file1 file2

diff -U 5 file1 file2

更紧凑的上下文显示输出模式,显示差异区域上下n行的文本,两个文件的差异块显示在一起

diff -y file1 file2

diff -y -W 140 file1 file2

side-by-side模式,分两部分分别显示两个文件

DT860 送到,有喜有忧

上周末到,已经run in 了20小时了,对比DT231 变化最大的是低频好不少,包围感上来了,中频相对突出,解析提升,整体性比DT231 偏暖。以上是优点。

…….缺点是没有想象中的好推,低频有点散,量也没有预想的多,高频提升不大,听交响提升并不大。最要命的是佩戴相当恶劣(这个现在感觉好多了),有点压头,夹头。不过就算这样我觉得也比DT880直推250欧要合适些吧(说实话坛子里已经很多人拍我了)

 

有很重要的一点忘了补充了,上述听感全部基于DAT D8 直推, 耳放就不折腾了,也不知道会有多大提升.