《linux情景分析 上》摘录
目录
以小写的.s文件是纯汇编语言。以大写的.S文件是吸收了C语言预处理的汇编文件,其中可以使用#include,#ifdef等成分,而数据结构也一样可以在.h文件中定义。
AT&T汇编和386汇编的语言格式差别:
1)在Intel格式中大多使用大写字母,而在AT&T格式中都使用小写字母。(个人感觉小写字母更好分辨,不喜欢大写的)
2)在AT&T格式中,寄存器名要加上“%”作为前缀,而在intel格式中则不带前缀。
3)在AT&T的386汇编预言中,指令的源操作数与目标操作数与intel的386汇编语言中正好相反。在intel格式中是目标在前,源在后,在AT&T中是源在前,目标在后;mov ebx, eax. mov %eax, %ebx. %eax -> %ebx.
4)在AT&T格式中,访问指令的操作数大小(宽度)由操作码名称的最后一个字母来决定,用操作码后缀的字母有b,w,l。而在intel格式中,则是在表示内存单元的操作数前面加上“BYTE PTR”, "WORD PTR" or “DWORD PTR”来表示。
MOV AL, BYTE PTR FOO
movb FOO, %al
5)在AT&T格式中,直接操作数要加上“$”作为前缀,而在intel格式中则不带前缀。所以Intel格式中的“PUSH 4”, 在AT&T中则变为“pushl $4”。
6)在AT&T格式中,绝对转移或调用指令jump/call的操作数(也即转移或调用的目标地址),要加上“*”作为前缀,而在intel格式中则不带。
7)远程的转移指令和子程序调用指令的操作码名称,在AT&T格式中为“ljmp”和“lcall”,而在intel格式中,则为“JMP FAR”和"CALL FAR"。当转移和调用的目标为直接操作数时,两种不同的表示如下:
CALL FAR SECTION : OFFSET
JMP FAR SECTION : OFFSET
lcall $section, $offset
ljmp $section, $offset
与之相应的远程返回指令,则为:
RET FAR STACK_ADJUST
lret $stack_adjust
8)间接寻址的一般格式,两者区别如下:
SECTION: [BASE+INDEX*SCALE+DISP]
section: disp(base, index, scale)
在AT&T格式中隐含了所进行的计算。例如, 当SECTION省略,INDEX和SCALE也省略,BASE为EBP,而DISP(位移)为4时,表示如下:
[ebp - 4]
-4(%ebp)
在AT&T格式的括号中如果只有一项base,就可以省略都好,否则不能省略,所以(%ebp)相当于(%ebp, ,),进一步相当于(%ebp, 0, 0),又如,当INDEX为EAX, SCALE为4(32位),DISP为foo,而其他均省略,则表示为:
[foo + EAX*4]
foo(, %eax, 4)
这种寻址方式常常用于在数据结构数组中访问特定元素内的一个字段,base为数组的起始地址,scale为每个数组元素的大小,index为下标。如果数组元素时数据结构,则disp为具体字段在结构中的位移。
嵌入C代码中的386汇编语言程序段
\n 是换行符
\t 表示TAB符
jmp 1f 表示往前找到第一个标号为1的那一行, 1b就表示往后找。
static __inline__ void atomic_add(int i, atomic_t *v)
{
__asm__ __volatilc__(
LOCK "addl %1, %0"
:"=m" (v->counter)
:"ir" (i), "m" (v->counter)
);
}
C代码中插入汇编代码的一般格式:
分为4部分,以“:”号加以分隔, 指令部: 输出部: 输入部 : 损坏部
第一部分就是汇编语句本身,其格式与在汇编语言程序设计中使用的基本相同,是必须有的。其他部分可视情况省略。
指令部中,数字加上前缀%,表示需要使用寄存器的样板操作数,由于gcc和gas在编译和汇编时根据后面的约束条件自行变通处理。由于这些样板操作数也使用”%“前缀,在涉及到具体的寄存器时就要在寄存器名前面加上两个”%“,以免混淆。 ”%%“
输出部,用以规定对输出变量,即目标操作数如何结合的约束条件。每个这样的条件称为一个”约束“,必要时输出部中可以有多个约束,互相以逗号分隔。每个输出约束以”=“号开头,然后一个字母表示对操作数类型的说明,然后是关于变量结合的约束。
输入部约束格式与输出很相似,但不带”=“号。
表示约束条件的字母有很多,主要有:
m, v, o ------表示内存单元
r, ------表示任何寄存器
q, ------表示寄存器eax, ebx, ecx, edx之一
i, h ------表示直接操作数
E, F ------表示浮点数
g ------表示“任意”
a, b, c, d ------分别表示要求使用寄存器:eax, ebx, ecx, edx
S, D ------分表表示要求使用寄存器: esi, edi
I ------表示常数0--31
当输出部为空,即没有输出约束时,如果有输入约束存在,则必须保留分隔标记”:“号。
例子如下:
extern inline char * strncpy(char * dest,const char *src,int count)
{
__asm__("cld\n"
"1:\tdecl %2\n\t"
"js 2f\n\t"
"lodsb\n\t"
"stosb\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"rep\n\t"
"stosb\n"
"2:"
::"S" (src),"D" (dest),"c" (count):"si","di","ax","cx");
return dest;
}
C中定义函数,汇编中实现
在asm.s中,很多中断都是以"__"开头的,如下
_divide_error:
pushl $_do_divide_error
no_error_code:
xchgl %eax,(%esp)
pushl %ebx
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs
pushl $0 # "error code"
lea 44(%esp),%edx
pushl %edx
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
call *%eax
addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret
这里是因为在把C函数编译成汇编代码的时候,汇编文件对当前函数比如add(),汇编的形式就是_add,所以在asm.s文件中每一个中断都是有一个c原型的,在traps.c里有
void divide_error(void);
void debug(void);
void nmi(void);
void int3(void);
void overflow(void);
void bounds(void);
void invalid_op(void);
void device_not_available(void);
void double_fault(void);
void coprocessor_segment_overrun(void);
void invalid_TSS(void);
void segment_not_present(void);
void stack_segment(void);
void general_protection(void);
定义的这些函数,都是没有函数体的,它在汇编中提供了对应的名称,然后在asm.s中进行了中断处理过程。
《linux内核完全注释 p277》
GNU汇编语言的32位寻址方式
AT&T,32位寻址的正规格式为:
immed32( basepointer, indexpointer, indexscale)
该格式寻址位置的计算方式为:immed32 + basepointer + indexpointer * indexscale
在应用时,并不需要写出所有这些字段,但immed32和basepointer之中必须有一个存在。以下时一些例子
1)对一个指定的C语言变量寻址
_booga
变量前的下划线时从会百年程序中得到静态(全局)C变量(booga)的方法。
2)对寄存器内容指向的寻址
%eax
3)通过寄存器中的内容作为基址寻址一个变量
_variable(%eax) #eax + _variable
4)一个整数数组中寻址一个值(比例值为4)
_array(, %eax, %4)
5)使用直接数寻址偏移量
对于C语言:*(p+1)其中p是字符的指针 char *
AT&T:格式: 1(%eax)其中eax中是p的值。
6)在一个8字节为一个记录的数组中寻址指定的字符。其中eax中是指定的记录号,ebx中式指定字符在记录中偏移址:
AT&T: _array(%ebx, %eax, 8) Intel: [ebx + eax * 8 + _array]