现在的位置: 首页 > 综合 > 正文

Essential Linux Device Driver附录A . Linux汇编

2013年10月21日 ⁄ 综合 ⁄ 共 4272字 ⁄ 字号 评论关闭

By 宋宝华 / 本系列文章交流与讨论:@宋宝华Barry

设备驱动程序有时需要用汇编实现一些代码片断,因此让我们看看Linux上汇编编程的不同特性。

图A.1显示了Linux在PC兼容系统上的引导顺序,是第2章“内核一瞥”中图2.1的缩减版。图中的固件组件是用不同的汇编语法实现的:

· BIOS通常全部用汇编编写。一些流行的PC BIOS使用像Microsoft Macro Assembler (MASM)这样的汇编来编码。

· Linux 引导程序,像LILO和GRUB用C与汇编混合编写。SYSLINUX引导程序整个用Netwide Assembler(NASM)汇编编写。

· 实模式的Linux启动代码使用GNU汇编器(GAS)编码。

· 保护模式的BIOS调用用内联汇编编写。内联汇编是GCC支持的结构,在C语句之间插入汇编。

图 A.1. 固件组件与汇编语法

clip_image002

在图A.1中,上面的两个组件通常遵守基于Intel的汇编语法,而下面的两个用AR&T(或GAS)语法来编码。也有一些例外,GRUB的汇编部分就使用GAS。

为了演示这两种语法之间的差异,考虑如下输出一个字节到并口的代码。在BIOS或引导程序所使用的Intel格式中,你将会编写代码:

mov dx, 03BCh ;0x3BC is the I/O address of the parallel port

mov al, 0ABh ;0xAB is the data to be output

out dx, al ;Send data to the parallel port

然而,如果你想从Linux实模式启动代码中完成同样的工作,你将需要编写如下代码:

movw $0x3BC, %dx

movb $0xAB, %al

outb %al, %dx

你会发现,不像Intel格式,在AT&T语法中,首先出现的是源操作数,目的操作数在其后。AT&T格式中的寄存器名字由%开始,立即数用$开始。AT&T的操作码为了指定内存操作数的宽度,都带有后缀如b(针对字节)和w(针对字);而Intel语法中通过查看操作数而不是操作码来实现此目的。在Intel语法中,为了移动指针引用,你需要为操作数指定前缀,如byte ptr。

学习AT&T语法的益处是它被GAS和内联GCC所支持,而GAS和GCC不仅运行于基于Intel的系统上,也运行于各种处理器架构。

下面,让我们使用GCC内联汇编重写前面的代码片断,它是你在保护模式的内核将要用到的:

unsigned short port = 0x3BC;

unsigned char data = 0xAB;

asm("outb %%al, %%dx\n\t"

:

: "a" (data), "d" (port)

);

GCC支持的汇编格式通常如下:

asm(assembly : output operand constraints : input operand constraints : clobbered operand specifier );

在操作数项,a,b,c,d,S和D分别代表EAX,EBX,ECX,EDX,ESI和EDI寄存器。输入操作数constraint用于在执行汇编指令之前,将数据从提供的变量里拷贝至寄存器。关于GCC内联汇编语法的细节请查看GCC 内联汇编指南(www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html)。

在我们的例子中,唯一用到的constraint是针对输入操作数的。此约束有效地拷贝data的值至AL寄存器,以及port的值至DX寄存器。在内联汇编中,寄存器名由%%开始,因为%被用于指定提供的操作数。%i代表第i个操作数,因此,在前面的例子内联汇编代码片断中,如果你想指定data和port,可以分别使用%0和%1。

为了对内联汇编转换有更清晰的了解,让我们看看对应于前面的内联汇编片断、通过提供-s命令行参数给GCC,由编译器产生的汇编代码。为了理解,请阅读针对产生的每行代码的注释:

movw $956, -2(%ebp) # Value of data in stack set to 0x3BC

movb $-85, -3(%ebp) # Value of port in stack set to 0xAB

movb -3(%ebp), %al # movb 0xAB, %al

movw -2(%ebp), %dx # movw 0x3BC, %dx

#APP # Marker to note start of inline assembly

outb %al, %dx # Write to parallel port

#NO_APP # Marker to note end of inline assembly

你也可以在用户模式的程序中使用内联汇编。下面是用内联汇编编写的一个应用程序,调用syslog()系统调用以从内核的printk()的环形缓冲区中读取最后的128字节:

#define READ_COMMAND 3 /* First argument to

syslog() system call */

#define MSG_LENGTH 128 /* Third argument to syslog() */

int

main(int argc, char *argv[])

{

int syslog_command = READ_COMMAND;

int bytes_to_read = MSG_LENGTH;

int retval;

char buffer[MSG_LENGTH]; /* Second argument to syslog() */

asm volatile(

"movl %1, %%ebx\n" /* READ_COMMAND */

"movl %2, %%ecx\n" /* buffer */

"movl %3, %%edx\n" /* bytes_to_read */

"movl $103, %%eax\n" /* __NR_syslog */

"int $128\n" /* Generate System Call */

"movl %%eax, %0" /* retval */

:"=r" (retval)

:"m"(syslog_command),"r"(buffer),"m"(bytes_to_read)

:"%eax","%ebx","%ecx","%edx");

if (retval > 0) printf("%s\n", buffer);

}

正如在第4章“打下基础”中所学到的,int $128(或者int 0x80)指令产生一个软中断,陷入系统调用。由于系统调用导致从用户模式至内核模式的转换,故函数参数未传入用户或内核堆栈中,而是在CPU寄存器中。此系统调用号(在include/asm-your-arch/unistd.h中有完整列表)存储在EAX寄存器中。对于syslog()系统调用,调用号是103。如果查看syslog()的参考页,将会发现它需要三个参数:命令,存放返回数据的缓冲区的地址,以及缓冲区的长度。这些分别通过EBX、ECX和EAX来传递。返回值被从EAX传递至retval。此内联汇编调用被转换为如下语句:

retval = syslog(syslog_command, buffer, bytes_to_read);

如果你编译并运行此代码,将会看到如下从内核的环形缓冲区中获取的输出:

0:0:0:0: Attached scsi removable disk sda

<5>sd 0:0:0:0: Attached scsi generic sg0 type 0

<7>usb-storage: device scan complete

...

arch/x86/kernel/entry_32.S中的所有内核系统调用trap会保存所有的寄存器内容至堆栈,因此 ,即使用户空间的代码使用CPU寄存器来传递参数,实际上系统调用处理函数还是从堆栈中取其参数,。为了确保系统调用例程预期的参数在堆栈中,都用GCC属性asmlinkage进行标记。需要注意的是asmlinkage与asm(或__asm__)没有任何关系,后者用于声明内联汇编。

让我们通过演示一个内联汇编的例子来结束本节,此例子修改自基于PowerPC的电路板的Linux引导程序。假设此电路板上的flash存储器不支持背景操作(BackGround Operation,BGO)。这意味此引导程序代码从flash执行时,不能写入flash;但有时这是必须的,例如如果引导程序需要更新内核映象,而此映象存放于flash的另一部分。一个解决方案是修改引导程序,以便用于写入和擦除flash的引导代码完全从指令cache(I-cache)中执行,而数据段放入数据cache(D-cache)中。示例用的GCC内联汇编编写的宏用于完成将必要的引导程序指令搬入I-cache的工作。为了理解此代码片断,你需要有一定的PowerPC汇编知识:

/* instr_length is the number of instructions to touch

into I-cache. _load_i$_copy and _end_i$_copy are

program labels */

#define load_into_icache_copy(instr_length) \

asm volatile("lis %%r3, 0x1@h\n \

ori %%r3, %%r3, 0x1@l\n \

mticcr %%r3\n \

isync\n \

\n \

lis %%r6, _end_i$_copy@h\n \

ori %%r6, %%r6, _end_i$_copy@l\n \

icbt %%r0, %%r6\n \

lis %%r4, %0@h\n \

ori %%r4, %%r4, %0@l\n \

mtctr %%r4\n \

_load_i$_copy: \

addis %%r6, %%r6, 32@ha\n \

addi %%r6, %%r6, 32@l\n \

icbt %%r0, %%r6\n \

bdnz _load_i$_copy\n \

_end_i$_copy: \

nop\n" \

: \

: "i"(instr_length) \

:"%r6","%r4","%r0","r8","r9");

调试

为了调试实模式内核,不能使用我们在第21章“调试设备驱动”中所讨论、使用过的调试器,如kdb或kgdb。调试内核汇编片断的便捷方式是将代码转换为Intel类型的语法后,使用DOS调试工具。但调试器是在16位时代编写的,因此,不能调试32位的代码,例如不能调试初始化EAX寄存器的代码。从Internet上可以下载一些32位的免费调试器。第21章所讨论的JTAG调试器是万金油,因为这一工具可用于调试BIOS,引导程序,Linux实模式代码,以及内核与BIOS之间的交互。

抱歉!评论已关闭.