GCC内联汇编的笔记

Posted on 六月 10th, 2010 in 笔记 | 2 Comments »

起比VC风格的内联汇编,GCC的确实要别扭些,一开始要不看手册肯定一头雾水。

int foo = 10, bar = 15;
asm volatile("addl  %%ebx,%%eax"
            :"=a"(foo)                //output constraint
            :"a"(foo), "b"(bar)      //input constraint
            :                            //clobbered registers(ignored)
);

指令大家都明白,不过:”=a”(foo)这样的语法就古怪了。:后面的东西好像叫做约束,指明了输出和输入中用到的变量和寄存器。第一个的”=a”(foo)是输出的约束,就表示汇编执行完毕后foo=a。后面的”a”(foo)是输入的约束,表示汇编执行前的a=foo。这一来C和汇编就可以在约束下边交换数据了。

刚才这个a就是表示分配eax寄存器。

各种约束还挺多的…

a,b,c,d 对应eax,ebx,ecx,edx
S,D 对应esi,edi
I 常数
q eax,ebx,ecx,edx中静态分配一个
r eax,ebx,ecx,edx,esi,edi中静态分配一个
m 内存定位
A 同时分配eax和ebx,形成一64位的寄存器
i 一个编译时确定的立即数。好像ljmp指令的第一个参数就必须得是立即数,比如ljmp $0×80, $label。如果ljmp ax, $label就绘出现一个“Error: suffix or operands invalid for ‘ljmp’的错误”

为什么要这么难看的语法呢…我猜这东西最早应该是给编译器而不是给人类设计的吧,比起VC风格的内联汇编,它可以得到更多关于变量和寄存器的信息,编译器分配起寄存器来可以心里有数,不用怕自作聪明的人类把事情都搞乱掉。

写一个bootsector

Posted on 五月 26th, 2010 in 备忘 | 4 Comments »

看的这个教程 http://share.solrex.org/WriteOS/

仿照《自己动手写操作系统》的格式,除却gas确实很囧的因素,大体上还是不错的。万事开头难,在开头的bootloader这里卡了好久的说…不过动手写一下也不过半小时的功夫 TvT

机器在启动的时候可能是遍历每个磁盘分区,若有发现第512字节位置是个0xAA55的魔数,就认为这是个引导的分区了。然后就把它的前512字节装入内存,从0×7c00位置开始执行。这就是最简单的引导方式了好像。

我们把汇编代码编译成一个512bytes的二进制文件,再把它放到一个软盘的映像里就好。

boot.S

[bits 16]                       ;real mode
[org 0x7c00]                  ;put code start at 0x7c00
[section .text]
 
_start:
    mov     ax, cs            ; init seg registers
    mov     ds, ax
    mov     es, ax
    call    _print_str      
 
_print_str:
    mov     ax, str            
    mov     cx, len    
    mov     bp, ax
    mov     bx, 0x000c
    mov     dl, 0
    mov     ax, 0x1301
    int     0x10                              ;int 0x10, just as manual says
 
_loop: 
    jmp     _loop                             ; forever loop
 
str: db      "screw you guys all fucked up~",10,13
len: equ     $-str
 
times 510-($-$$) db 0                   ; fill the rest with 0
dw 0xAA55                                  ; magic number

编译之

nasm -f bin boot.S

生成一个boot.bin,下一步搞个软盘镜像

dd if=boot.bin of=boot.img bs=512 count=1
dd if=/dev/zero of=boot.img skip=1 seek=1 bs=512 count=2879

ls -l 下,大约会是这样

-rwxr-xr-x 1 ssword ssword     512 2010-05-26 21:28 boot.bin
-rw-r--r-- 1 ssword ssword 1474560 2010-05-26 21:28 boot.img
-rw-r--r-- 1 ssword ssword     399 2010-05-26 21:28 boot.S

然后打开virtualbox,设置软驱映像为boot.img。启动虚拟机就可以看到一个可爱的”screw you all”什么的了 >v<

简介AT&T风格汇编

Posted on 二月 17th, 2010 in 翻译 | 14 Comments »

作者:vivek
翻译:ssword
原文:http://sig9.com/articles/att-syntax

本文粗谈一下gas(1)的汇编语法,即AT&T风格汇编。初次接触很可能会觉得它别扭,不过若有其他汇编语言的基础,稍事了解即可快速上手。我假设你熟悉INTEL风格汇编——也就是INTEL手册中的那种风格。方便起见,我就用NASM(Netwide Assembler)来做比对。

gas属于GNU Binary Utilities(binutils),也是GCC的一个后端。对编写较长的汇编程序而言它并非首选,不过对于类Unix系统的内核级hacking,它就无可替代了。选择AT&T风格使得gas饱受争议,人们总说它只是GCC的后端,而对开发者不友好。INTEL风格汇编的教众也认为,它在可读性及编译上几乎是令人窒息。尽管如此,有一点必须了解:很多操作系统都选择了gas作为底层代码的汇编器。

基本形式

AT&T汇编程序的结构与其他汇编大同小异,伪指令、标签、指令—即最多带三个操作数的助记符。要说AT&T汇编的不同,最显眼的地方就是它操作数的顺序。

例如,一个简单的数据移动指令在INTEL风格下边是这个样子:

mnemonic	destination, source

在AT&T风格下边则是这样:

mnemonic	source, destination

一部分人(包括我)觉得这种格式更贴切。接下来说说AT&T风格指令中的操作数。

寄存器

每个IA-32架构寄存器的名字必须以’%'作前缀,如%al,%bx,%ds,%cr0,等等。

mov	%ax, %bx

如上的mov指令就是把一个16位寄存器ax中的值移动到另一个16位寄存器bx中。

字面量

每个字面量必须以’$'为前缀。 例如:

mov	$100,	%bx
mov	$A,	%al

第一个指令是把值100移动到寄存器bx中,第二个指令是把一个字节A移动到AL寄存器中。下面这个指令就是错误的:

mov	%bx,	$100

怎么说呢,这条指令是要把寄存器bx的值移动给一个字面量,显然不靠谱。

内存寻址

在AT&T风格中,内存引用起来是这个格式:

segment-override:signed-offset(base,index,scale)

按你寻址的需求,其中的部分可以省略

%es:100(%eax,%ebx,2)

注意下,基地址及偏移中的数不带前缀’$'。拿几个例子和对应的NASM风格做个比较应该好些:

GAS memory operand			NASM memory operand
------------------			-------------------
 
100					[100]
%es:100					[es:100]
(%eax)					[eax]
(%eax,%ebx)				[eax+ebx]
(%ecx,%ebx,2)				[ecx+ebx*2]
(,%ebx,2)				[ebx*2]
-10(%eax)				[eax-10]
%ds:-10(%ebp)				[ds:ebp-10]

实例:

mov	%ax,	100
mov	%eax,	-100(%eax)

第一个指令是把寄存器AX中的值移动到数据段寄存器(默认)偏移100的内存位置,第二个指令是把寄存器eax中的值移动到[eax-100]。

操作数大小

有时指明操作数的大小是必须的,尤其是移动字面量到内存。例如这个指令:

mov	$10,	100

这里只说了把值10移动到内存偏址100处,而没有说值的大小。在NASM中,这通过给操作数后面跟个byte/word/dword之类的关键词来指明。而在AT&T风格里,是通过指令中b/w/l之类的后缀指明。如:

movb	$10,	%es:(%eax)

把值为10的一个字节移动到内存地址[ex:eax],另如:

movl	$10,	%es:(%eax)

把值为10的一个长整数移动到同一位置。

再几个例子:

movl	$100, %ebx
pushl	%eax
popw	%ax

控制流程

jmp,call,ret等指令可以转移程序的执行位置。在同一代码段中跳转,是近距跳转(near)。若是跳转到不同的代码段,就是远程跳转(far)。可用的跳转地址可以来自相对偏移(label)、寄存器、内存以及段偏移指针。相对偏移通过label指明,如下:

label1:
	.
	.
  jmp	label1

使用寄存器或者内存的值做地址的操作数必须加个前缀’*'。若是远程跳转,必须加个’l'作前缀,如‘ljmp’,‘lcall’等等。例如:

GAS syntax			NASM syntax
==========			===========
 
jmp	*100			jmp  near [100]
call	*100			call near [100]
jmp	*%eax			jmp  near eax
jmp	*%ecx			call near ecx
jmp	*(%eax)			jmp  near [eax]
call	*(%ebx)			call near [ebx]
ljmp	*100			jmp  far  [100]
lcall	*100			call far  [100]
ljmp	*(%eax)			jmp  far  [eax]
lcall	*(%ebx)			call far  [ebx]
ret				retn
lret				retf
lret $0x100			retf 0x100

段偏移指针按下面的格式指明:

jmp	$segment, $offset

例如:

jmp	$0x10, $0x100000

记住这些很快就能上手了。要了解gas的更多细节,不妨参阅这个文档

mips汇编入门

Posted on 十一月 3rd, 2009 in 笔记 | 19 Comments »

据说若要深入学习MIPS开发的话,《MIPS处理器设计透视》这书是必不可少的。不过若只是学习MIPS汇编,这书可能就不大合适了。汇编语言还是隐藏了CPU的很多细节,而这本书里讲的貌似就是这部分,在对汇编有所了解之后再来阅读可能要更好。

学习函数式语言的时候总是满足于看书,理解下语法语义即可,真正写的代码则少的可怜,不过确实“改变了编程的看法”,目的也算达到了。汇编就不行了,看书不够,一定得动手。所以学MIPS需要一个模拟器。pcspim应该是比较标准的了,不过感觉Mars可能要更好用(准确地说,Mars非常非常好用)。

MIPS是以优雅著称,据说即使是其竞争对手也如此认为。RISC么,32个寄存器,指令长度都一样。其中的指令大约这么三种形式:

j 1000
li $1, 10
add $1, $2,$3

差异就是各个参数的长度不同。如add指令的三个参数都只有两个位宽(0~255),每个参数表示一个寄存器。如果把指令看作函数,那参数就可以看作是有类型的。而MIPS的汇编器是很强大的(听说可以进行窥孔优化),像add $t0, $0, 10这样的指令会被汇编器翻译成addi $t0,$0,10。汇编器处理前后指令的对比可以在Mars中显示出来。

记几个helloworld吧,

求3的阶乘:

li $t0, 0
li $t1, 1
if_1:
add $t0, $t0, 1
mul $t1, $t1, $t0
bne $t0, 3, if_1

在Mars下可以看到寄存器的变化,最后$t1寄存器的值是6。

mips汇编的分支(branch)指令分b系(bne,beq,bgt等等)和j系(j, jr等),差别就是b系指令的跳转都是有条件的,而且地址在参数中指明,而j系的跳转都是无条件的。j系指令的地址长度更长,寻址范围要更大,所以远程跳转都是j。

输出Helloworld:

.text
.globl main
 
main:
li $v0, 4                     # just the print syscall in SPIM
la $a0, str
syscall
 
.data
str:
.asciiz "hello world"

这应该算个比较完整的汇编程序了。程序的可执行代码都是在.text段,数据在.data段。.globl指明程序的入口地址,那个:str指代的就是这段字符串的地址。字符串么,就是数组。数组不就是指针么。

其中这个syscall会与操作系统的不同而有差异。系统调用的号码由$v0指明,参数在$a系的寄存器中传递,返回值放回到$v0。这里调用的是spim实现的4号系统调用,即print string。

定义一个函数f_add,它可以将两个数相加:

.text
.globl main
 
f_add:
add $v0, $a0, $a1
jr $ra
 
main:
li $a0, 1
li $a1, 2
jal f_add
 
add $t0, $v0, $0
 
li $v0, 1
add $a0, $0, $t0
syscall

在使用jal指令的时候,它会把发生跳转的地址记录在$ra寄存器中。这样函数在结尾的时候就可以用jr返回原先的位置了。

使用寄存器传递参数的好处貌似就是约定了函数的调用规范,兼容性要更好。例如x86下的BASIC和C在参数传递时压栈的顺序貌似就是相反的。