Ted's Blog

Happy coding

LINUX汇编(汇编语言程序设计读书笔记)

#############################################
# 一, IA-32 硬件特性
#############################################

寄存器:
1, 通用寄存器, 用于存放正在处理的数据
EAX 用于操作数和结果数的累加器
EBX 指向数据内存断中的数据的指针
ECX 字符串和循环操作的计数器
EDX IO指针
EDI 用于字符串操作的目标的数据指针
ESI 用于字符串操作的源的数据指针
ESP 堆栈指针
EBP 堆栈数据指针

其中寄存器EAX, EBX, ECX, EDX又可以通过16位和8位寄存器名称引用
如EAX, AX 引用EAX低16位, AL 引用EAX低8位, AH 引用AL之后的高8位



2, 段寄存器:
IA-32平台允许使用3中内存模型: 平坦内存模式 分段内存模式 实地址模式

平坦内存: 把全部的系统内存表示为连续的地址空间, 通过线性地址的特定地址
访问内存位置.

分段内存: 把系统内存划分为独立的段组, 通过位于寄存器中的指针进行引用. 每
个段用于包含特定类型的数据。 一个段用于包含指令码, 另一个段包
含数据元素, 第三个段包含数据堆栈。
段中的内存位置是通过逻辑地址引用的, 逻辑地址是由段地址加上偏移
量构成, 处理器把逻辑地址转换为相应的线性地址以便访问。


段寄存器:
CS 代码段
DS 数据段
SS 堆栈段
ES 附加段指针
FS 附加段指针
GS 附加段指针

每个段寄存器都是16位的, 包含指向内存特定段起始位置的指针,程序不能
显示加载或改变CS寄存器, DS, ES, FS, GS都用于指向数据段, 通过4个独立
的段, 程序可以分隔数据元素, 确保他们不会重叠, 程序必须加载带有段的
正确指针值的数据段寄存器, 并且使用偏移值引用各个内存的位置。
SS段寄存器用于指向堆栈段, 堆栈包含传递给函数和过程的数据值。

实地址: 如果实地址模式, 所有段寄存器都指向线性0地址, 并且都不会被程序改动,
所有的指令码 数据元素 堆栈元素 都是通过他们的线性地址直接访问的。



3, 指令指针寄存器
是EIP寄存器, 它跟踪要执行程序的下一条指令代码, 应用程序不能修改指令指针本身,不
能指定内存地址把它拖放EIP寄存器中,相反必须通过一般的跳转指令来改变预存取缓存的
下一条指令。

在平坦内存模型中, 指令指针包含下一条指令码的线性地址, 在分段模型中指令指针包含
逻辑地址指针, 通过CS寄存器的内存引用。



4, 控制寄存器
CRO 控制操作模式 和 处理器当前状态的系统标志
CR1 当前没有使用
CR2 内存页面错误信息
CR3 内存页面目录信息
CR4 支持处理器特性和说明处理器特性能力的标志

不能直接访问控制寄存器, 但是能把控制寄存器中的值传递给通用寄存器,如果必须改动控制
寄存器的标志, 可以改动通用寄存器的值, 然后把内容传递给控制寄存器。





标志:
IA-32使用单一的寄存器来包含一组状态控制和系统标志, EFLAGS寄存器包含32位标志信息

1, 状态标志
标志 位 说明
CF 0 进位标志, 如果无符号数的数学操作产生最高有效位的进位或者借位, 此时值为1
PF 2 奇偶校验标志, 用于表明数学操作的结果寄存器中的是否包含错误数据
AF 4 辅助进位标志, 用于二进制编码的10进制(BCD)的数学操作中, 如果用于运算的
寄存器的第三位发生进位或借位, 该值为1
ZF 6 0标志, 如果操作为0, 则该值为1
SF 7 符号标志, 设置为结果的最高有效位, 这一位是符号位表明结果是正值还是负值
OF 11 溢出标志

2, 控制标志
当前只定义了一个控制标志DF即方向标志, 用于控制处理器处理字符串的方式
如果设置为1, 字符串指令自动递减内存地址以便到达字符串中的下一字节。
反之。

3, 系统标志
标志 位 说明
TF 8 陷阱标志, 设置为1时启用单步模式, 在单步模式下处理器每次只执行一条命令。
IF 9 中断使能标志, 控制处理器如响应从外部源接收到的信号。
IOPL 12和13 IO特权级别标志, 表明当前正在运行任务的IO特权级别, 它定义IO地址空间的
特权访问级别, 该值必须小于或者等于访问I/O地址空间的级别; 否则任何访问
IO空间的请求都会被拒绝!
NT 14 嵌套任务标志控制当前运行的任务是否连接到前一个任务, 它用于连接被中断
和被调用的任务.
RF 16 恢复标志用于控制在调试模式中如何响应异常。
VM 17 虚拟8086模式, 表明处理器在虚拟8086模式中而不是保护模式或者实模式。
AC 18 对准检查标志, 用于启用内存引用的对准检查
VIF 19 虚拟中断标志, 当处理器在虚拟模式中操作时, 该标志起IF标志的作用.
VIP 20 虚拟中断挂起标志, 在虚拟模式操作时用于表示一个中断正在被挂起。
ID 21 表示CPU是否支持cpuid指令, 如果处理器能够设置或者清零这个标志, 表示
处理器支持该指令。






################################################################################################
# 二,GNU汇编工具系列
################################################################################################
1, 二进制工具系列
addr2line 把地址转换成文件名或者行号

ar 创建 修改或者展开文件存档

as 把汇编语言代码汇编成目标代码
常用选项:
-a -> 指定输出中包含那些清单
-D -> 包含它用于向下兼容 但是被忽略
--defsym -> 在汇编代码之前定义符号和值
-f -> 快速汇编跳过注释和空白
--gstabs -> 包含每行源代码的调试信息
--gstats+ -> 包含gdb专门的调试信息
-I -> 指定包含文件的目录
-J -> 不警告带符号溢出
-L -> 在符号表中保存本地符号
-o -> 给定输出目标名
-R -> 把数据段合并进文本段
--statistics -> 显示汇编使用的最大空间和总时间
-v -> 显示as的版本号
-W -> 不显示警告信息

c++filt 还原c++符号的过滤器

gprof 显示程序简档信息的程序

ld 把目标代码文件转换成可执行文件的转换器
常用选项:
-d -> 指定目标代码输入文件的格式
-Bstatic -> 只使用静态库
-Bdynamic -> 只使用动态库
-Bsymbolic-> 把引用捆绑到共享库中的全局符号
-c -> 从指定的命令文件读取命令
-cref -> 创建跨引用表
-defsym -> 在输出文件中创建指定的全局符号
-demangle -> 在错误消息中还原符号名称
-e -> 使用指定的符号作为程序的初始执行点
-E -> 对于elf文件把所有的符号添加到动态符号表
-share -> 创建共享库
-Ttext -> 使用指定的地址作为文本段的起始点
-Tdata -> 使用指定的地址作为数据段的起始点
-Tbss -> 使用指定的地址作为bss段的起始点
-L -> 把指定的路径添加到库搜索清单
-O -> 生成优化的输出文件
-o -> 指定输出名
-oformat -> 指定输出文件的二进制格式
-R -> 从指定的文件读取符号和地址
-rpath -> 把指定的位置添加到运行时库搜索路径
-rpath-link-> 指定搜索运行时共享库的路径
-X -> 删除本地所有临时符号
-x -> 删除本地所有符号

nm 列出目标文件中的符号

objcopy 复制或翻译目标文件

objdump 显示来自目标文件的信息

ranlib 生成存档文件内容的索引

readelf 按照elf格式显示目标文件信息

size 列出目标文件或者存档文件的段长度

strings 显示目标文件中可打印字符串

strip 丢弃符号

windres 编译Microsoft Windows资源文件

2, GNU编译器
gcc
常用选项:
-c 编译或者汇编代码但不进行连接
-S 编译后停止但不进行汇编
-E 预处理后停止但不进行编译
-o 指定输出文件名
-v 显示每个编译阶段使用的命令
-std 指定使用的语言标准
-g 生成调试信息
-pg 生成gprof制作简档要使用的额外代码
-O 优化可执行代码
-W 设置编译器警告级别
-I 指定包含文件清单
-L 指定库文件目录
-D 预定义源代码中使用的宏
-U 取消任何定义了的宏
-f 指定控制编译器行为的选项
-m 指定与硬件相关的选项

3, GNU调试程序
gdb
常用选项:
-d 指定远程调试时串行接口的线路速度
-batch 以批处理模式运行
-c 指定要分析的核心转储文件
-cd 指定工作目录
-d 指定搜索源文件的目录
-e 指定要执行的文件
-f 调试时以标准格式输出文件名和行号
-q 安静模式
-s 指定符号的文件名
-se 指定符号和要执行的文件名
-tty 设置标准输出和输入设备
-x 从指定的文件执行gdb命令

由于gnu调试时忽略开始处断点, 需要在开始标签处执行一个空指令
如:
.globl _start
_start:
nop
此时断点可以设置成 break *_start+1
查看寄存器状态info registers
使用print命令查看特定寄存器或者变量的值, 加上修饰符可以得到不同的输出格式:
print/d 显示十进制数字
print/t 显示二进制数字
print/x 显示16进制数字
使用x命令可以查看特定内存的值:
x/nyz
其中 n为要显示的字段数
y时输出格式, 它可以是:
c 用于字符, d用于十进制, x用于16进制
z是要显示的字段长度, 它可以是:
b用于字节, h用于16字节, w用于32位字
如:
x/42cb 用于显示前42字节


################################################################################################
# 三, GNU汇编语言结构
################################################################################################
主要包括三个常用的段:
data 数据段 声明带有初始值的元素
bss 数据段 声明使用0或者null初始化的元素
text 正文段 包含的指令, 每个汇编程序都必须包含此段

使用.section 指令定义段, 如:
.section .data
.section .bss
.section .text

起始点:
gnu汇编器使用_start标签表示默认的起始点, 此外如果想要汇编内部的标签能够被外部程序访问,
需要使用.globl 指令, 如:.globl _start


使用通用库函数时可以使用:
ld -dynamic-linker /lib/ld-linux.so.2




################################################################################################
# 四, 数据传递
################################################################################################
1, 数据段
使用.data声明数据段, 这个段中声明的任何数据元素都保留在内存中并可以被汇编程序的指令读取,
此外还可以使用.rodata声明只读的数据段, 在声明一个数据元素时, 需要使用标签和命令:

标签:用做引用数据元素所使用的标记, 它和c语言的变量很相似, 它对于处理器是没有意义的, 它
只是用做汇编器试图访问内存位置时用做引用指针的一个位置。

指令:这个名字指示汇编器为通过标签引用的数据元素保留特定数量的内存, 声明命令之后必须给出
一个或多个默认值。

声明指令:
.ascii 文本字符串
.asciz 以空字符结尾的字符串
.byte 字节值
.double 双精度浮点值
.float 单精度浮点值
.int 32位整数
.long 32位整数, 和int相同
.octa 16字节整数
.quad 8字节整数
.short 16位整数
.single 单精度浮点数(和float相同)


例子:
output:
.ascii "hello world."

pi:
.float 2.14

声明可以在一行中定义多个值, 如:
ages:
.int 20, 10, 30, 40


定义静态符号:
使用.equ命令把常量值定义为可以在文本段中使用的符号,如:
.section .data
.equ LINUX_SYS_CALL, 0x80
.section .text
movl $LINUX_SYS_CALL, %eax



2, bss段
和data段不同, 无需声明特定的数据类型, 只需声明为所需目的保留的原始内存部分即可。
GNU汇编器使用以下两个命令声明内存区域:
.comm 声明为未初始化的通用内存区域
.lcomm 声明为未初始化的本地内存区域

两种声明很相似, 但.lcomm是为不会从本地汇编代码之外进行访问的数据保留的, 格式为:
.comm/.lcomm symbol, length

例子:
.section .bss
.lcomm buffer, 1000
该语句把1000字节的内存地址赋予标签buffer, 在声明本地通用内存区域的程序之外的函数是
不能访问他们的.(不能在.globl命令中使用他们)


在bss段声明的好处是, 数据不包含在可执行文件中。在数据段中定义数据时, 它必须被包含在
可执行程序中, 因为必须使用特定值初始化它。 因为不使用数据初始化bss段中声明的数据区域,
所以内存区域被保留在运行时使用, 并且不必包含在最终的程序中




3, 传送数据
move 指令:
格式 movex 源操作数, 目的操作数。 其中x为要传送数据的长度, 取值有:
l 用于32位的长字节
w 用于16位的字
b 用于8位的字节值


立即数前面要加一个$符号, 寄存器前面要加%符号。

8个通用的寄存器是用于保存数据的最常用的寄存器, 这些寄存器的内容可以传递
给其他的任何可用的寄存器。 和通用寄存器不同, 专用寄存器(控制, 调试, 段)
的内容只能传送给通用寄存器, 或者接收从通用寄存器传过来的内容。


在对标签进行引用时:
例:
.section .data
value:
.int 100
_start:
movl value, %eax
movl $value, %eax
movl %ebx, (%edi)
movl %ebx, 4(%edi)

其中:movl value, %eax 只是把标签value当前引用的内存值传递给eax
movl $value, %eax 把标签value当前引用的内存地址指针传递给eax
movl %ebx, (%edi) 如果edi外面没有括号那么这个指令只是把ebx中的
值加载到edi中, 如果有了括号就表示把ebx中的内容
传送给edi中包含的内存位置。
movl %ebx, 4(%edi) 表示把edi中的值放在edi指向的位置之后的4字节内存位置中
movl %ebx, -4(%edi) 表示把edi中的值放在edi指向的位置之前的4字节内存位置中



cmove 指令(条件转移):
cmovex 源操作数, 目的操作数. x的取值为:
无符号数:
a/nbe 大于/不小于或者等于
ae/nb 大于或者等于/不小于
nc 无进位
b/nae 小于/不大于等于
c 进位
be/na 小于或等于/不大于
e/z 等于/零
ne/nz 不等于/不为零
p/pe 奇偶校验/偶校验
np/po 非奇偶校验/奇校验

有符号数:
ge/nl 大于或者等于/不小于
l/nge 小于/不大于或者等于
le/ng 小于或者等于/不大于
o 溢出
no 未溢出
s 带符号(负)
ns 无符号(非负)





交换数据:
xchg 在两个寄存器之间或者寄存器和内存间交换值
如:
xchg 操作数, 操作数, 要求两个操作数必须长度相同且不能同时都是内存位置
其中寄存器可以是32,16,8位的


bswap 反转一个32位寄存器的字节顺序

如: bswap %ebx


xadd 交换两个值 并把两个值只和存储在目标操作数中

如: xadd 源操作数,目标操作数
其中源操作数必须是寄存器, 目标操作数可以是内存位置也可以是寄存器
其中寄存器可以是32,16,8位的

cmpxchg
cmpxchg source, destination
其中source必须是寄存器, destination可以是内存或者寄存器, 用来比较
两者的值, 如果相等,就把源操作数的值加载到目标操作数中, 如果不等就把
目标操作数加载到源操作数中,其中寄存器可以是32,16,8位的, 其中源操作
数是EAX,AX或者AL寄存器中的值


cmpxchg8b 同cmpxchg, 但是它处理8字节值, 同时它只有一个操作数
cmpxchg8b destination
其中destination引用一个内存位置, 其中的8字节值会与EDX和EAX寄存器中
包含的值(EDX高位寄存器, EAX低位寄存器)进行比较, 如果目标值和EDX:EAX
对中的值相等, 就把EDX:EAX对中的64位值传递给内存位置, 如果不匹配就把
内存地址中的值加载到EDX:EAX对中



4, 堆栈
ESP 寄存器保存了当前堆栈的起始位置, 当一个数据压入栈时, 它就会自动递减,
反之其自动递增

压入堆栈操作:
pushx source, x取值为:
l 32位长字
w 16位字

弹出堆栈操作:
popx source
其中source必须是16或32位寄存器或者内存位置, 当pop最后一个元素时ESP值应该
和以前的相等


5,压入和弹出所有寄存器
pusha/popa 压入或者弹出所有16位通用寄存器
pushad/popad 压入或者弹出所有32位通用寄存器
pushf/popf 压入或者弹出EFLAGS寄存器的低16位
pushfd/popfd 压入或者弹出EFLAGS寄存器的全部32位


6,数据地址对齐
gas 汇编器支持.align 命令, 它用于在特定的内存边界对准定义的数据元素, 在数据段
中.align命令紧贴在数据定义的前面




################################################################################################
# 五,控制流程
################################################################################################
无条件跳转:
1, 跳转
jmp location 其中location为要跳转到的内存地址, 在汇编中为定义的标签

2,调用
调用指令分为两个部分:
1, 调用call address 跳转到指定位置
2, 返回指令ret, 它没有参数紧跟在call指令后面的位置

执行call指令时,它把EIP的值放到堆栈中, 然后修改EIP以指向被调用的函数地址, 当被调用
函数完成后, 它从堆栈获取过去的EIP的值, 并把控制权返还给原始程序。

3,中断
由硬件设备生成中断。 程序生成软件中断
当一个程序产生中断调用时, 发出调用的程序暂停, 被调用的程序接替它运行, 指令指针被转移到
被调用的函数地址, 当调用完成时使用中断返回指令可以返回调原始程序。



条件跳转:
条件跳转按照EFLAGS中的值来判断是否该跳转, 格式为:

jxx address, 其中xx是1-3个字符的条件代码, 取值如下:
a 大于时跳转
ae 大于等于
b 小于
be 小于等于
c 进位
cxz 如果CX寄存器为0
ecxz 如果ECS寄存器为0
e 相等
na 不大于
nae 不大于或者等于
nb 不小于
nbe 不小于或等于
nc 无进位
ne 不等于
g 大于(有符号)
ge 大于等于(有符号)
l 小于(有符号)
le 小于等于(有符号)
ng 不大于(有符号)
nge 不大于等于(有符号)
nl 不小于
nle 不小于等于
no 不溢出
np 不奇偶校验
ns 无符号
nz 非零
o 溢出
p 奇偶校验
pe 如果偶校验
po 如果奇校验
s 如果带符号
z 如果为零


条件跳转不支持分段内存模型下的远跳转, 如果在该模式下进行
程序设计必须使用程序逻辑确定条件是否存在, 然后实现无条件
跳转, 跳转前必须设置EFLAGS寄存器



比较:
cmp operend1, operend2


进位标志修改指令:
CLC 清空进位标志(设置为0)
CMC 对进位标志求反(把它改变为相反的值)
STC 设置进位标志(设置为1)


循环:
loop 循环直到ECX寄存器为0
loope/loopz 循环直到ecx寄存器为0 或者没有设置ZF标志
loopne/loopnz 循环直到ecx为0或者设置了ZF标志

指令格式为: loopxx address 注意循环指令只支持8位偏移地址



################################################################################################
# 六,数字
################################################################################################

IA-32平台中存储超过一字节的数都被存储为小尾数的形式但是把数字传递给寄存器时, 寄存器里面保存是按照大尾数
的形式存储


把无符号数转换成位数更大的值时, 必须确保所有的高位部分都被设置为零

把有符号数转换成位数更大的数时:
intel 提供了movsx指令它允许扩展带符号数并保留符号, 它与movzx相似, 但是它假设要传送的字节是带符号数形式


浮点数:
fld 指令用于把浮点数字传送入和传送出FPU寄存器, 格式:
fld source
其中source可以为32 64或者80位整数值


IA-32使用FLD指令用于把存储在内存中的单精度和双精度浮点值FPU寄存器堆栈中, 为了区分这两种长度GNU汇编器使用
FLDS加载单精度浮点数, FLDL加载双精度浮点数

类似FST用于获取FPU寄存器堆栈中顶部的值, 并且把这个值放到内存位置中, 对于单精度使用FSTS, 对于双精度使用FSTL



################################################################################################
# 七,基本数学运算
################################################################################################
1, 加法
ADD source, destination 把两个整数相加
其中source可以是立即数内存或者寄存器, destination可以是内存或者寄存器, 但是两者不能同时都是内存位置


ADC 和ADD相似进行加法运算, 但是它把前一个ADD指令的产生进位标志的值包含在其中, 在处理位数大于32(如64)
位的整数时, 该指令非常有用

2, 减法
SUB source, destination 把两个整数相减
NEG 它生成值的补码
SBB 指令, 和加法操作一样, 可以使用进位情况帮助执行大的无符号数值的减法运算. SBB在多字节减法操作中利用
进位和溢出标志实现跨数据边界的的借位特性

3,递增和递减
dec destination 递减
inc destination 递增

其中dec和inc指令都不会影响进位标志, 所以递增或递减计数器的值都不会影响程序中涉及进位标志的其他任何运算

4, 乘法
mul source 进行无符号数相乘
它使用隐含的目标操作数, 目标位置总是使用eax的某种形式, 这取决与源操作数的长度, 因此根据源操作数的长度,
目标操作数必须放在AL, AX, EAX中。 此外由于乘法可能产生很大的值, 目标位置必须是源操作数的两倍位置, 源为
8时, 应该是16, 源为16时, 应该为32, 但是当源为16位时intel为了向下兼容, 目标操作数不是存放在eax中, 而
是分别存放在DX:AX中, 结果高位存储在DX中, 地位存储在AX中。对于32位的源, 目标操作数存储在EDX:EAX中, 其中
EDX存储的是高32位, EAX存储的是低32位

imul source 进行有符号数乘法运算, 其中的目标操作数和mul的一样

imul source, destination 也可以执行有符号乘法运算, 但是此时可以把目标放在指定的位置, 使用这种格式的缺陷
在与乘法的操作结果被限制为单一目标寄存器的长度.

imul multiplier, source, destination
其中multiplier是一个立即数, 这种方式允许一个值与给定的源操作数进行快速的乘法运算, 然后把结果存储在通用
寄存器中


5, 除法
div divisor 执行无符号数除法运算
除数的最大值取决与被除数的长度, 对于16位被除数 ,除数只能为8位, 32或64位同上
被除数 被除数长度 商 余数
AX 16位 AL AH
DX:AX 32位 AX DX
EDX:EAX 64位 EAX EDX

idiv divisor 执行有符号数的除法运算, 方式和div一样

6, 移位
左移位:
sal 向左移位
sal destination 把destination向左移动1位
sal %cl, destination 把destination的值向左移动CL寄存器中指定的位数
sal shifter, destination 把destination的值向左移动shifter值指定的位数
向左移位可以对带符号数和无符号数执行向左移位的操作, 移位造成的空位用零填充, 移位造成的超过数据长度的任何位
都被存放在进位标志中, 然后在下一次移位操作中被丢弃

右移位:
shr向右移位
sar向右移位
SHR指令清空移位造成的空位, 所以它只能对无符号数进行移位操作
SAR指令根据整数的符号位, 要么清空, 要么设置移位造成的空位, 对于负数, 空位被设置为1

循环移位:
和移位指令类似, 只不过溢出的位被存放回值的另一端, 而不是丢弃
ROL 向左循环移位
ROR 向右循环移位
RCL 向左循环移位, 并且包含进位标志
RCR 向右循环移位, 并且包含进位标志

7, 逻辑运算
AND OR XOR
这些指令使用相同的格式:
and source, destination
其中source可以是8位 16 位或者32位的立即值 寄存器或内存中的值, destination可以是8位 16 位或者
32位寄存器或内存中的值, 不能同时使用内存值作为源和目标。 布尔逻辑功能对源和目标执行按位操作。
也就是说使用指定的逻辑功能按照顺序对数据的元素的每个位进行单独比较。

NOT指令使用单一操作数, 它即是源值也是目标结果的位置
清空寄存器的最高效方式是使用OR指令对寄存器和它本身进行异或操作.当和本身进行XOR操作时, 每个设置为
1的位就变为0, 每个设置为0的位也变位0。

位测试可以使用以上的逻辑运算指令, 但这些指令会修改destination的值, 因此intel提供了test指令, 它不
会修改目标值而是设置相应的标志



################################################################################################
# 八,字符串处理
################################################################################################
1, 传送字符串
movs 有三种格式
movsb 传送单一字节
movsw 传送一个字
movsl 传送双字

movs指令使用隐含的源和目的操作数, 隐含的源操作数是ESI, 隐含的目的操作数是EDI, 有两种方式加载内存地址到
ESI和EDI, 第一种是使用标签间接寻址 movl $output, %ESI, 第二种是使用lea指令, lea指令加载对象的地址到指定
的目的操作数如lea output, %esi, 每次执行movs指令后, 数据传送后ESI和EDI寄存器会自动改变,为另一次传送做
准备, ESI和EDI可能随着标志DF的不同自动递增或者自动递减, 如果DF标志为0则movs指令后ESI和EDI会递增, 反之会
递减, 为了设置DF标志, 可以使用一下指令:
CLD 将DF标志清零
STD 设置DF标志

2,rep前缀
REP 指令的特殊之处在与它不执行什么操作, 这条指令用于按照特定次数重复执行字符串指令, 有ECX寄存器控制,
但不需要额外的loop指令, 如rep movsl

rep的其他格式:
repe 等于时重复
repne 不等于时重复
repnz 不为零时重复
repz 为零时重复

3, 存储和加载字符串
LODS 加载字符串, ESI为源, 当一次执行完lods时会递增或递减ESI寄存器, 然后把字符串值存放到EAX中

STOS 使用lods把字符串值加载到EAX后, 可以使用它把EAX中的值存储到内存中去:
stos使用EDI作为目的操作数, 执行stos指令后, 会根据DF的值自动递增或者递减EDI中的值

4, 比较字符串
cmps 和其他的操作字符串的指令一样, 隐含的源和目标操作数都为ESI和EDI, 每次执行时都会根据DF的值把
ESI和EDI递增或者递减, cmps指令从目标字符串中减去源字符串, 执行后会设置EFLAGS寄存器的状态.


5,扫描字符串
scas 把EDI作为目标, 它把EDI中的字符串和EAX中的字符串进行比较 ,然后根据DF的值递增或者递减EDI



################################################################################################
# 九,使用函数
################################################################################################
GNU汇编语言定义函数的语法:
.type 标签(也就是函数名), @function
ret 返回到调用处



################################################################################################
# 十,linux系统调用
################################################################################################
linux系统调用的中断向量为0x80

1, 系统调用标识存放在%eax中
2, 系统调用输入值:
EBX 第一个参数
ECX 第二个参数
EDX 第三个参数
ESI 第四个参数
EDI 第五个参数

需要输入超过6个输入参数的系统调用, EBX指针用于保存指向输入参数内存位置的指针, 输入参数按照连续的的顺序
存储, 系统调用的返回值存放在EAX中

################################################################################################
# 十一,汇编语言的高级功能
################################################################################################
1,gnu内联汇编的语法:
asm或__asm__("汇编代码");
指令必须包含在引号里
如果包含的指令超过一行 必须使用新行分隔符分隔


使用c全局变量, 不能在内联汇编中使用局部变量, 注意在汇编语言代码中值被用做内存位置, 而不是立即数值

如果不希望优化内联汇编, 则可以volatile修饰符如:__asm__ volatile("code");

2,GCC内联汇编的扩展语法
__asm__("assembly code":output locations:input operands:changed registers);
第一部分是汇编代码
第二部分是输出位置, 包含内联汇编代码的输出值的寄存器和内存位置列表
第三部分是输入操作数,包含内联汇编代码输入值的寄存器和内存位置的列表
第四部分是改动的寄存器, 内联汇编改变的任何其他寄存器的列表
这几个部分可以不全有, 但是没有的还必须使用:分隔

1, 指定输入值和输出值, 输入值和输出值的列表格式为:
"constraint"(variable), 其中variable是程序中声明的c变量, 在扩展asm格式中, 局部和全局变量都可以使用,
使用constrant(约束)定义把变量存放到哪(输入)或从哪里传送变量(输出)
约束使用单一的字符, 如下:
约束 描述
a 使用%eax, %ax, %al寄存器
b 使用%ebx, %bx, %bl寄存器
c 使用%ecx, %cx, %cl寄存器
d 使用%edx, %dx, %dl寄存器
S 使用%esi, %si寄存器
D 使用%edi, %di寄存器
r 使用任何可用的通用寄存器
q 使用%eax, %ebx, %ecx,%edx之一
A 对于64位值使用%eax, %edx寄存器
f 使用浮点寄存器
t 使用第一个(顶部)的浮点寄存器
u 使用第二个浮点寄存器
m 使用变量的内存位置
o 使用偏移内存位置
V 只使用直接内存位置
i 使用立即整数值
n 使用值已知的立即整数值
g 使用任何可用的寄存器和内存位置


除了这些约束之外, 输出值还包含一个约束修饰符:
输出修饰符 描述
+ 可以读取和写入操作数
= 只能写入操作数
% 如果有必要操作数可以和下一个操作数切换
& 在内联函数完成之前, 可以删除和重新使用操作数

如:
__asm__("assembly code": "=a"(result):"d"(data1),"c"(data2));
把c变量data1存放在edx寄存器中, 把c变量data2存放到ecx寄存器中, 内联汇编的结果
将存放在eax寄存器中, 然后传送给变量result


在扩展的asm语句块中如果要使用寄存器必须使用两个百分号符号


不一定总要在内联汇编代码中指定输出值, 一些汇编指令假定输入值包含输出值, 如movs指令


其他扩展内联汇编知识:
1, 使用占位符
输入值存放在内联汇编段中声明的特定寄存器中, 并且在汇编指令中专门使用这些寄存器.
虽然这种方式能够很好的处理只有几个输入值的情况, 但对于需要很多输入值的情况, 这
中方式显的有点繁琐. 为了帮助解决这个问题, 扩展asm格式提供了占位符, 可以在内联
汇编代码中使用它引用输入和输出值.

占位符是前面加上百分号的数字, 按照内联汇编中列出的每个输入和输出值在列表中的位置,
每个值被赋予从0开始的地方. 然后就可以在汇编代码中引用占位符来表示值。


如果内联汇编代码中的输入和输出值共享程序中相同的c变量, 则可以指定使用占位符作为
约束值, 如:
__asm__("imull %1, %0"
: "=r"(data2)
: "r"(data1), "0"(data2));
如输入输出值中共享相同的变量data2, 而在输入变量中则可以使用标记0作为输入参数的约束

2, 替换占位符
如果处理很多输入和输出值, 数字型的占位符很快就会变的很混乱, 为了使条理清晰 ,GNU汇编
器(从版本3.1开始)允许声明替换的名称作为占位符.替换的名称在声明输入值和输出值的段中
定义, 格式如下:
%[name]"constraint"(variable)
定义的值name成为内联汇编代码中变量的新的占位符号标识, 如下面的例子:
__asm__("imull %[value1], %[value2]"
: [value2] "=r"(data2)
: [value1] "r"(data1), "0"(data2));

3, 改动寄存器列表
编译器假设输入值和输出值使用的寄存器会被改动, 并且相应的作出处理。程序员不需要在改动的
寄存器列表中包含这些值, 如果这样做了, 就会产生错误消息. 注意改动的寄存器列表中的寄存器
使用完整的寄存器名称, 而不像输入和输出寄存器定义的那样仅仅是单一字母。 在寄存器名称前面
使用百分号符号是可选的。

改动寄存器列表的正确使用方法是, 如果内联汇编代码使用了没有被初始化地声明为输入或者输出
值的其他任何寄存器 , 则要通知编译器。编译器必须知道这些寄存器, 以避免使用他们。如:
int main(void) {
int data1 = 10;
int result = 20;

__asm__("movl %1, %%eax\n\t"
"addl %%eax, %0"
: "=r"(result)
: "r"(data1), "0"(result)
: "%eax");
printf("The result is %d\n", result);
return 0;
}

4, 使用内存位置
虽然在内联汇编代码中使用寄存器比较快, 但是也可以直接使用c变量的内存位置。 约束m用于引用输入值
和输出值中的内存位置。 记住, 对于要求使用寄存器的汇编指令, 仍然必须使用寄存器, 所以不得不定义
保存数据的中间寄存器。如:
int main(void) {
int dividentd = 20;
int divisor = 5;
int result;

__asm__("divb %2\n\t"
"movl %%eax, %0"
: "=m"(result)
: "a"(dividend), "m"(divisor));
printf("The result is %d\n", result);
return 0;
}

5, 处理跳转
内联汇编语言代码也可以包含定义其中位置的标签。 可以实现一般的汇编条件分支和无条件分支, 如:
int main(void) {
int a = 10;
int b = 20;
int result;

__asm__("cmp %1, %2\n\t"
"jge greater\n\t"
"movl %1, %0\n\t"
"jmp end\n"
"greater:\n\t"
"movl %2, %0\n"
"end:"
:"=r"(result)
:"r"(a), "r"(b));
printf("The larger value is %d\n", result);
return 0;
}

在内联汇编代码中使用标签时有两个限制。 第一个限制是只能跳转到相同的asm段内的标签,
不能从-个asm段跳转到另一个asm段中的标签。第二个限制更加复杂一点。 以上程序使用
标签greater和end。 但是, 这样有个潜在的问题, 查看汇编后的代码清单, 可以发现内联
汇编标签也被编码到了最终汇编后的代码中。 这意味着如果在c代码中还有另一个asm段, 就
不能再次使用相同的标签, 否则会因为标签重复使用而导致错误消息。还有如果试图整合使用
c关键字(比如函数名称或者全局变量)的标签也会导致错误。



################################################################################################
# 十二,优化你的代码
################################################################################################
GNU编译器提供-O选项供程序优化使用:
-O 提供基础级别的优化
-O2 提供更加高级的代码优化
-O3 提供最高级的代码优化
不同的优化级别使用的优化技术也可以单独的应用于代码。 可以使用-f命令行选项引用每个
单独的优化技术。

1, 编译器优化级别1
在优化的第一个级别执行基础代码的优化。 这个级别试图执行9种单独的优化功能:
-fdefer-pop: 这种优化技术与汇编语言代码在函数完成时如何进行操作有关。 一般
情况下, 函数的输入值被保存在堆栈种并且被函数访问。 函数返回时, 输入值还在
堆栈种。 一般情况下, 函数返回之后, 输入值被立即弹出堆栈。这样做会使堆栈种
的内容有些杂乱。

-fmerge-constans: 使用这种优化技术, 编译器试图合并相同的常量. 这一特性有
时候会导致很长的编译时间, 因为编译器必须分析c或者c++程序中用到的每个常量,
并且相互比较他们.

-fthread-jumps: 使用这种优化技术与编译器如果处理汇编代码中的条件和非条件
分支有关。 在某些情况下, 一条跳转指令可能转移到另一条分支语句。 通过一连串
跳转, 编译器确定多个跳转之间的最终目标并且把第一个跳转重新定向到最终目标。

-floop-optimize: 通过优化如何生成汇编语言中的循环, 编译器可以在很大程序上
提高应用程序的性能。 通常, 程序由很多大型且复杂的循环构成。 通过删除在循环
内没有改变值的变量赋值操作, 可以减少循环内执行指令的数量, 在很大程度上提高
性能。 此外优化那些确定何时离开循环的条件分支, 以便减少分支的影响。

-fif-conversion: if-then语句应该是应用程序中仅次于循环的最消耗时间的部分。
简单的if-then语句可能在最终的汇编语言代码中产生众多的条件分支。 通过减少
或者删除条件分支, 以及使用条件传送 设置标志和使用运算技巧来替换他们, 编译
器可以减少if-then语句中花费的时间量。

-fif-conversion2: 这种技术结合更加高级的数学特性, 减少实现if-then语句所
需的条件分支。

-fdelayed-branch: 这种技术试图根据指令周期时间重新安排指令。 它还试图把
尽可能多的指令移动到条件分支前, 以便最充分的利用处理器的治理缓存。

-fguess-branch-probability: 就像其名称所暗示的, 这种技术试图确定条件分支最可
能的结果, 并且相应的移动指令, 这和延迟分支技术类似。 因为在编译时预测代码的安排,
所以使用这一选项两次编译相同的c或者c++代码很可能会产生不同的汇编语言代码, 这取决
于编译时编译器认为会使用那些分支。 因为这个原因, 很多程序员不喜欢采用这个特性, 并且
专门地使用-fno-guess-branch-probability选项关闭这个特性

-fcprop-registers: 因为在函数中把寄存器分配给变量, 所以编译器执行第二次检查以便减少
调度依赖性(两个段要求使用相同的寄存器)并且删除不必要的寄存器复制操作。

2, 编译器优化级别2
结合了第一个级别的所有优化技术, 再加上一下一些优化:
-fforce-mem: 这种优化再任何指令使用变量前, 强制把存放再内存位置中的所有变量都复制到寄存器
中。 对于只涉及单一指令的变量, 这样也许不会有很大的优化效果. 但是对于再很多指令(必须数学操作)
中都涉及到的变量来说, 这会时很显著的优化, 因为和访问内存中的值相比 ,处理器访问寄存器中的值要
快的多。

-foptimize-sibling-calls: 这种技术处理相关的和/或者递归的函数调用。 通常, 递归的函数调用
可以被展开为一系列一般的指令, 而不是使用分支。 这样处理器的指令缓存能够加载展开的指令并且
处理他们, 和指令保持为需要分支操作的单独函数调用相比, 这样更快。

-fstrength-reduce: 这种优化技术对循环执行优化并且删除迭代变量。 迭代变量是捆绑到循环计数器
的变量, 比如使用变量, 然后使用循环计数器变量执行数学操作的for-next循环。

-fgcse: 这种技术对生成的所有汇编语言代码执行全局通用表达式消除历程。 这些优化操作试图分析
生成的汇编语言代码并且结合通用片段, 消除冗余的代码段。如果代码使用计算性的goto, gcc指令推荐
使用-fno-gcse选项。

-fcse-follow-jumps: 这种特别的通用子表达式消除技术扫描跳转指令, 查找程序中通过任何其他途径都不会到达的目标代码。 这种情况最常见的例子就式if-then-else语句的else部分。

-frerun-cse-after-loop: 这种技术在对任何循环已经进行过优化之后重新运行通用子表达式消除例程。
这样确保在展开循环代码之后更进一步地优化还编代码。

-fdelete-null-pointer-checks: 这种优化技术扫描生成的汇编语言代码, 查找检查空指针的代码。 编译器假设间接引用空指针将停止程序。 如果在间接引用之后检查指针, 它就不可能为空。

-fextensive-optimizations: 这种技术执行从编译时的角度来说代价高昂的各种优化技术,但是它可能
对运行时的性能产生负面影响。

-fregmove: 编译器试图重新分配mov指令中使用的寄存器, 并且将其作为其他指令操作数, 以便最大化
捆绑的寄存器的数量。

-fschedule-insns: 编译器将试图重新安排指令, 以便消除等待数据的处理器。 对于在进行浮点运算时有
延迟的处理器来说, 这使处理器在等待浮点结果时可以加载其他指令。

-fsched-interblock: 这种技术使编译器能够跨越指令块调度指令。 这可以非常灵活地移动指令以便等待
期间完成的工作最大化。

-fcaller-saves: 这个选项指示编译器对函数调用保存和恢复寄存器, 使函数能够访问寄存器值, 而且不必
保存和恢复他们。 如果调用多个函数, 这样能够节省时间, 因为只进行一次寄存器的保存和恢复操作, 而
不是在每个函数调用中都进行。

-fpeephole2: 这个选项允许进行任何计算机特定的观察孔优化。

-freorder-blocks: 这种优化技术允许重新安排指令块以便改进分支操作和代码局部性。

-fstrict-aliasing: 这种技术强制实行高级语言的严格变量规则。 对于c和c++程序来说, 它确保不在数据类型之间共享变量. 例如, 整数变量不和单精度浮点变量使用相同的内存位置。

-funit-at-a-time: 这种优化技术指示编译器在运行优化例程之前读取整个汇编语言代码。 这使编译器可以
重新安排不消耗大量时间的代码以便优化指令缓存。 但是, 这会在编译时花费相当多的内存, 对于小型计算机可能
是一个问题。

-falign-functions: 这个选项用于使函数对准内存中特定边界的开始位置。 大多数处理器按照页面读取内存,并且确保全部函数代码位于单一内存页面内, 就不需要叫化代码所需的页面。

-fcrossjumping: 这是对跨越跳转的转换代码处理, 以便组合分散在程序各处的相同代码。 这样可以减少
代码的长度, 但是也许不会对程序性能有直接影响。

3, 编译器优化级别3
它整合了第一和第二级别中的左右优化技巧, 还包括一下优化:
-finline-functions: 这种优化技术不为函数创建单独的汇编语言代码, 而是把函数代码包含在调度程序的代码中。 对于多次被调用的函数来说, 为每次函数调用复制函数代码。 虽然这样对于减少代码长度不利, 但是通过最充分的利用指令缓存代码, 而不是在每次函数调用时进行分支操作, 可以提高性能。

-fweb: 构建用于保存变量的伪寄存器网络。 伪寄存器包含数据, 就像他们是寄存器一样, 但是可以使用各种其他优化技术进行优化, 比如cse和loop优化技术。

-fgcse-after-reload: 这中技术在完全重新加载生成的且优化后的汇编语言代码之后执行第二次gcse优化,帮助消除不同优化方式创建的任何冗余段。

Linux 汇编语言开发指南

汇编语言的优点是速度快,可以直接对硬件进行操作,这对诸如图形处理等关键应用是非常重要的。Linux 是一个用 C 语言开发的操作系统,这使得很多程序员开始忘记在 Linux 中还可以直接使用汇编这一底层语言来优化程序的性能。本文为那些在Linux 平台上编写汇编代码的程序员提供指南,介绍 Linux 汇编语言的语法格式和开发工具,并辅以具体的例子讲述如何开发实用的Linux 汇编程序。

一、简介

作为最基本的编程语言之一,汇编语言虽然应用的范围不算很广,但重要性却勿庸置疑,因为它能够完成许多其它语言所无法完成的功能。就拿 Linux 内核来讲,虽然绝大部分代码是用 C 语言编写的,但仍然不可避免地在某些关键地方使用了汇编代码,其中主要是在 Linux 的启动部分。由于这部分代码与硬件的关系非常密切,即使是 C 语言也会有些力不从心,而汇编语言则能够很好扬长避短,最大限度地发挥硬件的性能。

大多数情况下 Linux 程序员不需要使用汇编语言,因为即便是硬件驱动这样的底层程序在 Linux 操作系统中也可以用完全用 C 语言来实现,再加上 GCC 这一优秀的编译器目前已经能够对最终生成的代码进行很好的优化,的确有足够的理由让我们可以暂时将汇编语言抛在一边了。但实现情况是 Linux 程序员有时还是需要使用汇编,或者不得不使用汇编,理由很简单:精简、高效和 libc 无关性。假设要移植 Linux 到某一特定的嵌入式硬件环境下,首先必然面临如何减少系统大小、提高执行效率等问题,此时或许只有汇编语言能帮上忙了。

汇编语言直接同计算机的底层软件甚至硬件进行交互,它具有如下一些优点:

  • 能够直接访问与硬件相关的存储器或 I/O 端口;
  • 能够不受编译器的限制,对生成的二进制代码进行完全的控制;
  • 能够对关键代码进行更准确的控制,避免因线程共同访问或者硬件设备共享引起的死锁;
  • 能够根据特定的应用对代码做最佳的优化,提高运行速度;
  • 能够最大限度地发挥硬件的功能。

同时还应该认识到,汇编语言是一种层次非常低的语言,它仅仅高于直接手工编写二进制的机器指令码,因此不可避免地存在一些缺点:

  • 编写的代码非常难懂,不好维护;
  • 很容易产生 bug,难于调试;
  • 只能针对特定的体系结构和处理器进行优化;
  • 开发效率很低,时间长且单调。

Linux 下用汇编语言编写的代码具有两种不同的形式。第一种是完全的汇编代码,指的是整个程序全部用汇编语言编写。尽管是完全的汇编代码,Linux 平台下的汇编工具也吸收了 C 语言的长处,使得程序员可以使用 #include、#ifdef 等预处理指令,并能够通过宏定义来简化代码。第二种是内嵌的汇编代码,指的是可以嵌入到C语言程序中的汇编代码片段。虽然 ANSI 的 C 语言标准中没有关于内嵌汇编代码的相应规定,但各种实际使用的 C 编译器都做了这方面的扩充,这其中当然就包括 Linux 平台下的 GCC。



回页首

二、Linux 汇编语法格式

绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同:

  1. 在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。例如:

    AT&T 格式 Intel 格式
    pushl %eax push eax
  2. 在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。例如:

    AT&T 格式 Intel 格式
    pushl $1 push 1
  3. AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。例如:

    AT&T 格式 Intel 格式
    addl $1, %eax add eax, 1
  4. 在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。例如:

    AT&T 格式 Intel 格式
    movb val, %al mov al, byte ptr val
  5. 在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel 格式中则不需要。
  6. 远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 "ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即:

    AT&T 格式 Intel 格式
    ljump $section, $offset jmp far section:offset
    lcall $section, $offset call far section:offset

    与之相应的远程返回指令则为:

    AT&T 格式 Intel 格式
    lret $stack_adjust ret far stack_adjust
  7. 在 AT&T 汇编格式中,内存操作数的寻址方式是

    section:disp(base, index, scale)
    

    而在 Intel 汇编格式中,内存操作数的寻址方式为:

    section:[base + index*scale + disp]
    

    由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:

    disp + base + index * scale
    

    下面是一些内存操作数的例子:

    AT&T 格式 Intel 格式
    movl -4(%ebp), %eax mov eax, [ebp - 4]
    movl array(, %eax, 4), %eax mov eax, [eax*4 + array]
    movw array(%ebx, %eax, 4), %cx mov cx, [ebx + 4*eax + array]
    movb $4, %fs:(%eax) mov fs:eax, 4



回页首

三、Hello World!

真不知道打破这个传统会带来什么样的后果,但既然所有程序设计语言的第一个例子都是在屏幕上打印一个字符串 "Hello World!",那我们也以这种方式来开始介绍 Linux 下的汇编语言程序设计。

在 Linux 操作系统中,你有很多办法可以实现在屏幕上显示一个字符串,但最简洁的方式是使用 Linux 内核提供的系统调用。使用这种方法最大的好处是可以直接和操作系统的内核进行通讯,不需要链接诸如 libc 这样的函数库,也不需要使用 ELF 解释器,因而代码尺寸小且执行速度快。

Linux 是一个运行在保护模式下的 32 位操作系统,采用 flat memory 模式,目前最常用到的是 ELF 格式的二进制代码。一个 ELF 格式的可执行程序通常划分为如下几个部分:.text、.data 和 .bss,其中 .text 是只读的代码区,.data 是可读可写的数据区,而 .bss 则是可读可写且没有初始化的数据区。代码区和数据区在 ELF 中统称为 section,根据实际需要你可以使用其它标准的 section,也可以添加自定义 section,但一个 ELF 可执行程序至少应该有一个 .text 部分。 下面给出我们的第一个汇编程序,用的是 AT&T 汇编语言格式:

例1. AT&T 格式

#hello.s 
.data                    # 数据段声明
        msg : .string "Hello, world!\\n" # 要输出的字符串
        len = . - msg                   # 字串长度
.text                    # 代码段声明
.global _start           # 指定入口函数
        
_start:                  # 在屏幕上显示一个字符串
        movl $len, %edx  # 参数三:字符串长度
        movl $msg, %ecx  # 参数二:要显示的字符串
        movl $1, %ebx    # 参数一:文件描述符(stdout) 
        movl $4, %eax    # 系统调用号(sys_write) 
        int  $0x80       # 调用内核功能
        
                         # 退出程序
        movl $0,%ebx     # 参数一:退出代码
        movl $1,%eax     # 系统调用号(sys_exit) 
        int  $0x80       # 调用内核功能

初次接触到 AT&T 格式的汇编代码时,很多程序员都认为太晦涩难懂了,没有关系,在 Linux 平台上你同样可以使用 Intel 格式来编写汇编程序:

例2. Intel 格式

; hello.asm 
section .data            ; 数据段声明
        msg db "Hello, world!", 0xA     ; 要输出的字符串
        len equ $ - msg                 ; 字串长度
section .text            ; 代码段声明
global _start            ; 指定入口函数
_start:                  ; 在屏幕上显示一个字符串
        mov edx, len     ; 参数三:字符串长度
        mov ecx, msg     ; 参数二:要显示的字符串
        mov ebx, 1       ; 参数一:文件描述符(stdout) 
        mov eax, 4       ; 系统调用号(sys_write) 
        int 0x80         ; 调用内核功能
                         ; 退出程序
        mov ebx, 0       ; 参数一:退出代码
        mov eax, 1       ; 系统调用号(sys_exit) 
        int 0x80         ; 调用内核功能

上面两个汇编程序采用的语法虽然完全不同,但功能却都是调用 Linux 内核提供的 sys_write 来显示一个字符串,然后再调用 sys_exit 退出程序。在 Linux 内核源文件 include/asm-i386/unistd.h 中,可以找到所有系统调用的定义。



回页首

四、Linux 汇编工具

Linux 平台下的汇编工具虽然种类很多,但同 DOS/Windows 一样,最基本的仍然是汇编器、连接器和调试器。

1.汇编器

汇编器(assembler)的作用是将用汇编语言编写的源程序转换成二进制形式的目标代码。Linux 平台的标准汇编器是 GAS,它是 GCC 所依赖的后台汇编工具,通常包含在 binutils 软件包中。GAS 使用标准的 AT&T 汇编语法,可以用来汇编用 AT&T 格式编写的程序:

[xiaowp@gary code]$ as -o hello.o hello.s

Linux 平台上另一个经常用到的汇编器是 NASM,它提供了很好的宏指令功能,并能够支持相当多的目标代码格式,包括 bin、a.out、coff、elf、rdf 等。NASM 采用的是人工编写的语法分析器,因而执行速度要比 GAS 快很多,更重要的是它使用的是 Intel 汇编语法,可以用来编译用 Intel 语法格式编写的汇编程序:

[xiaowp@gary code]$ nasm -f elf hello.asm

2.链接器

由汇编器产生的目标代码是不能直接在计算机上运行的,它必须经过链接器的处理才能生成可执行代码。链接器通常用来将多个目标代码连接成一个可执行代码,这样可以先将整个程序分成几个模块来单独开发,然后才将它们组合(链接)成一个应用程序。 Linux 使用 ld 作为标准的链接程序,它同样也包含在 binutils 软件包中。汇编程序在成功通过 GAS 或 NASM 的编译并生成目标代码后,就可以使用 ld 将其链接成可执行程序了:

[xiaowp@gary code]$ ld -s -o hello hello.o

3.调试器

有人说程序不是编出来而是调出来的,足见调试在软件开发中的重要作用,在用汇编语言编写程序时尤其如此。Linux 下调试汇编代码既可以用 GDB、DDD 这类通用的调试器,也可以使用专门用来调试汇编代码的 ALD(Assembly Language Debugger)。

从调试的角度来看,使用 GAS 的好处是可以在生成的目标代码中包含符号表(symbol table),这样就可以使用 GDB 和 DDD 来进行源码级的调试了。要在生成的可执行程序中包含符号表,可以采用下面的方式进行编译和链接:

[xiaowp@gary code]$ as --gstabs -o hello.o hello.s
[xiaowp@gary code]$ ld -o hello hello.o

执行 as 命令时带上参数 --gstabs 可以告诉汇编器在生成的目标代码中加上符号表,同时需要注意的是,在用 ld 命令进行链接时不要加上 -s 参数,否则目标代码中的符号表在链接时将被删去。

在 GDB 和 DDD 中调试汇编代码和调试 C 语言代码是一样的,你可以通过设置断点来中断程序的运行,查看变量和寄存器的当前值,并可以对代码进行单步跟踪。图1 是在 DDD 中调试汇编代码时的情景:


图1 用 DDD 中调试汇编程序

汇编程序员通常面对的都是一些比较苛刻的软硬件环境,短小精悍的ALD可能更能符合实际的需要,因此下面主要介绍一下如何用ALD来调试汇编程序。首先在命令行方式下执行ald命令来启动调试器,该命令的参数是将要被调试的可执行程序:

[xiaowp@gary doc]$ ald hello
Assembly Language Debugger 0.1.3
Copyright (C) 2000-2002 Patrick Alken
hello: ELF Intel 80386 (32 bit), LSB, Executable, Version 1 (current)
Loading debugging symbols...(15 symbols loaded)
ald>

当 ALD 的提示符出现之后,用 disassemble 命令对代码段进行反汇编:

ald> disassemble -s .text
Disassembling section .text (0x08048074 - 0x08048096)
08048074  BA0F000000                 mov edx, 0xf
08048079  B998900408                 mov ecx, 0x8049098
0804807E  BB01000000                 mov ebx, 0x1
08048083  B804000000                 mov eax, 0x4
08048088  CD80                       int 0x80
0804808A  BB00000000                 mov ebx, 0x0
0804808F  B801000000                 mov eax, 0x1
08048094  CD80                       int 0x80

上述输出信息的第一列是指令对应的地址码,利用它可以设置在程序执行时的断点:

ald> break 0x08048088
Breakpoint 1 set for 0x08048088

断点设置好后,使用 run 命令开始执行程序。ALD 在遇到断点时将自动暂停程序的运行,同时会显示所有寄存器的当前值:

ald> run
Starting program: hello
Breakpoint 1 encountered at 0x08048088
eax = 0x00000004 ebx = 0x00000001 ecx = 0x08049098 edx = 0x0000000F
esp = 0xBFFFF6C0 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds  = 0x0000002B es  = 0x0000002B fs  = 0x00000000 gs  = 0x00000000
ss  = 0x0000002B cs  = 0x00000023 eip = 0x08048088 eflags = 0x00000246
Flags: PF ZF IF
08048088  CD80                       int 0x80

如果需要对汇编代码进行单步调试,可以使用 next 命令:

ald> next
Hello, world!
eax = 0x0000000F ebx = 0x00000000 ecx = 0x08049098 edx = 0x0000000F
esp = 0xBFFFF6C0 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds  = 0x0000002B es  = 0x0000002B fs  = 0x00000000 gs  = 0x00000000
ss  = 0x0000002B cs  = 0x00000023 eip = 0x0804808F eflags = 0x00000346
Flags: PF ZF TF IF
0804808F  B801000000                 mov eax, 0x1

若想获得 ALD 支持的所有调试命令的详细列表,可以使用 help 命令:

ald> help
Commands may be abbreviated.
If a blank command is entered, the last command is repeated.
Type `help <command>' for more specific information on <command>.
General commands
attach         clear          continue       detach         disassemble
enter          examine        file           help           load
next           quit           register       run            set
step           unload         window         write
Breakpoint related commands
break          delete         disable        enable         ignore
lbreak         tbreak



回页首

五、系统调用

即便是最简单的汇编程序,也难免要用到诸如输入、输出以及退出等操作,而要进行这些操作则需要调用操作系统所提供的服务,也就是系统调用。除非你的程序只完成加减乘除等数学运算,否则将很难避免使用系统调用,事实上除了系统调用不同之外,各种操作系统的汇编编程往往都是很类似的。

在 Linux 平台下有两种方式来使用系统调用:利用封装后的 C 库(libc)或者通过汇编直接调用。其中通过汇编语言来直接调用系统调用,是最高效地使用 Linux 内核服务的方法,因为最终生成的程序不需要与任何库进行链接,而是直接和内核通信。

和 DOS 一样,Linux 下的系统调用也是通过中断(int 0x80)来实现的。在执行 int 80 指令时,寄存器 eax 中存放的是系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi 中,当系统调用完成之后,返回值可以在寄存器 eax 中获得。

所有的系统调用功能号都可以在文件 /usr/include/bits/syscall.h 中找到,为了便于使用,它们是用 SYS_<name> 这样的宏来定义的,如 SYS_write、SYS_exit 等。例如,经常用到的 write 函数是如下定义的:

ssize_t write(int fd, const void *buf, size_t count);

该函数的功能最终是通过 SYS_write 这一系统调用来实现的。根据上面的约定,参数 fb、buf 和 count 分别存在寄存器 ebx、ecx 和 edx 中,而系统调用号 SYS_write 则放在寄存器 eax 中,当 int 0x80 指令执行完毕后,返回值可以从寄存器 eax 中获得。

或许你已经发现,在进行系统调用时至多只有 5 个寄存器能够用来保存参数,难道所有系统调用的参数个数都不超过 5 吗?当然不是,例如 mmap 函数就有 6 个参数,这些参数最后都需要传递给系统调用 SYS_mmap:

void  *  mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);

当一个系统调用所需的参数个数大于 5 时,执行int 0x80 指令时仍需将系统调用功能号保存在寄存器 eax 中,所不同的只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器 ebx 中保存指向该内存区域的指针。系统调用完成之后,返回值仍将保存在寄存器 eax 中。

由于只是需要一块连续的内存区域来保存系统调用的参数,因此完全可以像普通的函数调用一样使用栈(stack)来传递系统调用所需的参数。但要注意一点,Linux 采用的是 C 语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即最后一个参数先入栈,而第一个参数则最后入栈。如果采用栈来传递系统调用所需的参数,在执行int 0x80 指令时还应该将栈指针的当前值复制到寄存器 ebx中。



回页首

六、命令行参数

在 Linux 操作系统中,当一个可执行程序通过命令行启动时,其所需的参数将被保存到栈中:首先是 argc,然后是指向各个命令行参数的指针数组 argv,最后是指向环境变量的指针数据 envp。在编写汇编语言程序时,很多时候需要对这些参数进行处理,下面的代码示范了如何在汇编代码中进行命令行参数的处理:

例3. 处理命令行参数

# args.s
.text
.globl _start
        
_start:
        popl	%ecx		# argc
vnext:
        popl	%ecx		# argv
        test 	%ecx, %ecx      # 空指针表明结束
        jz	exit
        movl	%ecx, %ebx
        xorl	%edx, %edx
strlen:
        movb	(%ebx), %al
        inc	%edx
        inc	%ebx
        test	%al, %al
        jnz	strlen
        movb	$10, -1(%ebx)
        movl	$4, %eax        # 系统调用号(sys_write) 
        movl	$1, %ebx        # 文件描述符(stdout) 
        int	$0x80
        jmp	vnext
exit:
        movl	$1,%eax         # 系统调用号(sys_exit) 
        xorl	%ebx, %ebx      # 退出代码
        int 	$0x80
		
        ret



回页首

七、GCC 内联汇编

用汇编编写的程序虽然运行速度快,但开发速度非常慢,效率也很低。如果只是想对关键代码段进行优化,或许更好的办法是将汇编指令嵌入到 C 语言程序中,从而充分利用高级语言和汇编语言各自的特点。但一般来讲,在 C 代码中嵌入汇编语句要比"纯粹"的汇编语言代码复杂得多,因为需要解决如何分配寄存器,以及如何与C代码中的变量相结合等问题。

GCC 提供了很好的内联汇编支持,最基本的格式是:

__asm__("asm statements");

例如:

__asm__("nop"); 

如果需要同时执行多条汇编语句,则应该用"\\n\\t"将各个语句分隔开,例如:

__asm__( "pushl %%eax \\n\\t"
         "movl $0, %%eax \\n\\t"
         "popl %eax");

通常嵌入到 C 代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到完整的内联汇编格式:

__asm__("asm statements" : outputs : inputs : registers-modified);

插入到 C 代码中的汇编语句是以":"分隔的四个部分,其中第一部分就是汇编代码本身,通常称为指令部,其格式和在汇编语言中使用的格式基本相同。指令部分是必须的,而其它部分则可以根据实际情况而省略。

在将汇编语句嵌入到C代码中时,操作数如何与C代码中的变量相结合是个很大的问题。GCC采用如下方法来解决这个问题:程序员提供具体的指令,而对寄存器的使用则只需给出"样板"和约束条件就可以了,具体如何将寄存器与变量结合起来完全由GCC和GAS来负责。

在GCC内联汇编语句的指令部中,加上前缀'%'的数字(如%0,%1)表示的就是需要使用寄存器的"样板"操作数。指令部中使用了几个样板操作数,就表明有几个变量需要与寄存器相结合,这样GCC和GAS在编译和汇编时会根据后面给定的约束条件进行恰当的处理。由于样板操作数也使用'%'作为前缀,因此在涉及到具体的寄存器时,寄存器名前面应该加上两个'%',以免产生混淆。

紧跟在指令部后面的是输出部,是规定输出变量如何与样板操作数进行结合的条件,每个条件称为一个"约束",必要时可以包含多个约束,相互之间用逗号分隔开就可以了。每个输出约束都以'='号开始,然后紧跟一个对操作数类型进行说明的字后,最后是如何与变量相结合的约束。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行完嵌入的汇编代码后均不保留执行之前的内容,这是GCC在调度寄存器时所使用的依据。

输出部后面是输入部,输入约束的格式和输出约束相似,但不带'='号。如果一个输入约束要求使用寄存器,则GCC在预处理时就会为之分配一个寄存器,并插入必要的指令将操作数装入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行完嵌入的汇编代码后也不保留执行之前的内容。

有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容。在GCC内联汇编格式中的最后一个部分中,可以对将产生副作用的寄存器进行说明,以便GCC能够采用相应的措施。

下面是一个内联汇编的简单例子:

例4.内联汇编

/* inline.c */
int main()
{
    int a = 10, b = 0;
    __asm__ __volatile__("movl %1, %%eax;\\n\\r"
                         "movl %%eax, %0;"
                         :"=r"(b)      /* 输出 */    
                         :"r"(a)       /* 输入 */
                         :"%eax");     /* 不受影响的寄存器 */
    
    printf("Result: %d, %d\\n", a, b);
}

上面的程序完成将变量a的值赋予变量b,有几点需要说明:

  • 变量b是输出操作数,通过%0来引用,而变量a是输入操作数,通过%1来引用。
  • 输入操作数和输出操作数都使用r进行约束,表示将变量a和变量b存储在寄存器中。输入约束和输出约束的不同点在于输出约束多一个约束修饰符'='。
  • 在内联汇编语句中使用寄存器eax时,寄存器名前应该加两个'%',即%%eax。内联汇编中使用%0、%1等来标识变量,任何只带一个'%'的标识符都看成是操作数,而不是寄存器。
  • 内联汇编语句的最后一个部分告诉GCC它将改变寄存器eax中的值,GCC在处理时不应使用该寄存器来存储任何其它的值。
  • 由于变量b被指定成输出操作数,当内联汇编语句执行完毕后,它所保存的值将被更新。

在内联汇编中用到的操作数从输出部的第一个约束开始编号,序号从0开始,每个约束记数一次,指令部要引用这些操作数时,只需在序号前加上'%'作为前缀就可以了。需要注意的是,内联汇编语句的指令部在引用一个操作数时总是将其作为32位的长字使用,但实际情况可能需要的是字或字节,因此应该在约束中指明正确的限定符:

限定符 意义
"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)



回页首

八、小结

Linux操作系统是用C语言编写的,汇编只在必要的时候才被人们想到,但它却是减少代码尺寸和优化代码性能的一种非常重要的手段,特别是在与硬件直接交互的时候,汇编可以说是最佳的选择。Linux提供了非常优秀的工具来支持汇编程序的开发,使用GCC的内联汇编能够充分地发挥C语言和汇编语言各自的优点。

参考资料

  1. 在网站 http://linuxassembly.org上可以找到大量的Linux汇编资源。
  2. 软件包binutils提供了as和ld等实用工具,其相关信息可以在网站 http://sources.redhat.com/binutils/上找到。
  3. NASM是Intel格式的汇编器,其相关信息可以在网站 http://nasm.sourceforge.net上找到。
  4. ALD是一个短小精悍的汇编调试器,其相关信息可以在网站 http://dunx1.irt.drexel.edu/~psa22/ald.html上找到。
  5. intel2gas是一个能够将Intel汇编格式转换成AT&T汇编格式的小工具,其相关信息可以在网站 http://www.niksula.cs.hut.fi/~mtiihone/intel2gas/上找到。
  6. IBM developerWorks上有一篇介绍GCC内联汇编的文章( http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.shtml)。
  7. 本文代码下载: 代码

AT&T汇编语法

0.3.1 Overview 

 

开 发一个OS,尽管绝大部分代码只需要用C/C++等高级语言就可以了,但至少和硬件相关部分的代码需要使用汇编语言,另外,由于启动部分的代码有大小限 制,使用精练的汇编可以缩小目标代码的Size。另外,对于某些需要被经常调用的代码,使用汇编来写可以提高性能。所以我们必须了解汇编语言,即使你有可 能并不喜欢它。

 

如果你是计算机专业的话,在大学里你应该学习过Intel格式的8086/80386汇编,这里就不再讨论。如果我们选择的OS开发工具是GCC以及GAS的话,就必须了解AT&T汇编语言语法,因为GCC/GAS只支持这种汇编语法。

 

本书不会去讨论8086/80386的汇编编程,这类的书籍很多,你可以参考它们。这里只会讨论AT&T的汇编语法,以及GCC的内嵌汇编语法。


0.3.2 Syntax 

1.寄存器引用

引用寄存器要在寄存器号前加百分号%,如“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)。

2. 操作数顺序

操作数排列是从源(左)到目的(右),如“movl %eax(源), %ebx(目的)”

3. 立即数

使用立即数,要在数前面加符号$, 如“movl $0x04, %ebx”

或者:

para = 0x04

movl $para, %ebx

指令执行的结果是将立即数04h装入寄存器ebx。

4. 符号常数

符号常数直接引用 如

value: .long 0x12a3f2de

movl value , %ebx

指令执行的结果是将常数0x12a3f2de装入寄存器ebx。

引用符号地址在符号前加符号$, 如“movl $value, % ebx”则是将符号value的地址装入寄存器ebx。

5. 操作数的长度

操作数的长度用加在指令后的符号表示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”。

6. 符号扩展和零扩展指令

绝大多数面向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-extend byte in %al to word in %ax;
  • cwde -- sign-extend word in %ax to long in %eax;
  • cwd -- sign-extend word in %ax to long in %dx:%ax;
  • cdq -- sign-extend dword in %eax to quad in %edx:%eax;

对应的AT&T语法的指令为cbtw,cwtl,cwtd,cltd。

7. 调用和跳转指令

段内调用和跳转指令为"call","ret"和"jmp",段间调用和跳转指令为"lcall","lret"和"ljmp"。

段间调用和跳转指令的格式为“lcall/ljmp $SECTION, $OFFSET”,而段间返回指令则为“lret $STACK-ADJUST”。

8. 前缀

操作码前缀被用在下列的情况:

  • 字符串重复操作指令(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”次。

9. 内存引用

Intel语法的间接内存引用的格式为:

section:[base+index*scale+displacement]

而在AT&T语法中对应的形式为:

section:displacement(base,index,scale)

其中,base和index是任意的32-bit base和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)。

AT&T汇编

在阅读linux内核源代码的时候,必须先掌握汇编,大家都知道,内核代码用的编译器是gcc,而gcc采用的是AT&T的汇编格式,与MS的intel有些区别。

一 AT&T的基本语法

    语法上主要有以下几个不同. 

★ 寄存器命名原则 

AT&T: %eax Intel: eax 

★ 源/目的操作数顺序 

AT&T: movl %eax,%ebx Intel: mov ebx,eax 

★ 常数/立即数的格式 

AT&T: movl $_value,%ebx Intel: mov eax,_value 

把_value的地址放入eax寄存器 

AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d 

★ 操作数长度标识 

AT&T: movw %ax,%bx Intel: mov bx,ax 

★寻址方式 

AT&T: immed32(basepointer,indexpointer,indexscale) 

Intel: [basepointer + indexpointer*indexscale + imm32) 

Linux工作于保护模式下,用的是32位线性地址,所以在计算地址时 

不用考虑segment:offset的问题.上式中的地址应为: 

imm32 + basepointer + indexpointer*indexscale 

下面是一些例子: 

★直接寻址 

AT&T: _booga ; _booga是一个全局的C变量 

注意加上$是表示地址引用,不加是表示值引用. 

注:对于局部变量,可以通过堆栈指针引用. 

Intel: [_booga] 

★寄存器间接寻址 

AT&T: (%eax) 

Intel: [eax] 

★变址寻址 

AT&T: _variable(%eax) 

Intel: [eax + _variable] 

AT&T: _array(,%eax,4) 

Intel: [eax*4 + _array] 

AT&T: _array(%ebx,%eax,8) 

Intel: [ebx + eax*8 + _array] 


二 基本的行内汇编 

    基本的行内汇编很简单,一般是按照下面的格式 

asm("statements"); 

例如:asm("nop"); asm("cli"); 

asm 和 __asm__是完全一样的. 

如果有多行汇编,则每一行都要加上 "\n\t" 

例如: 

asm( "pushl %eax\n\t" 

"movl $0,%eax\n\t" 

"popl %eax"); 

实际上gcc在处理汇编时,是要把asm(...)的内容"打印"到汇编 

文件中,所以格式控制字符是必要的. 

再例如: 

asm("movl %eax,%ebx"); 

asm("xorl %ebx,%edx"); 

asm("movl $0,_booga); 

在上面的例子中,由于我们在行内汇编中改变了edx和ebx的值,但是 

由于gcc的特殊的处理方法,即先形成汇编文件,再交给GAS去汇编, 

所以GAS并不知道我们已经改变了edx和ebx的值,如果程序的上下文 

需要edx或ebx作暂存,这样就会引起严重的后果.对于变量_booga也 

存在一样的问题.为了解决这个问题,就要用到扩展的行内汇编语法. 


三 扩展的行内汇编 

    扩展的行内汇编类似于Watcom. 

基本的格式是: 

asm ( "statements" : output_regs : input_regs : clobbered_regs); 

clobbered_regs指的是被改变的寄存器. 

下面是一个例子(为方便起见,我使用全局变量): 

int count=1; 

int value=1; 

int buf[10]; 

void main() 

asm( 

"cld \n\t" 

"rep \n\t" 

"stosl" 

:  "c" (count), "a" (value) , "D" (buf[0]) 

:  "%ecx","%edi" ); 

得到的主要汇编代码为: 

movl count,%ecx 

movl value,%eax 

movl buf,%edi 

#APP 

cld 

rep 

stosl 

#NO_APP 

cld,rep,stos就不用多解释了. 

这几条语句的功能是向buf中写上count个value值. 

冒号后的语句指明输入,输出和被改变的寄存器. 

通过冒号以后的语句,编译器就知道你的指令需要和改变哪些寄存器, 

从而可以优化寄存器的分配. 

其中符号"c"(count)指示要把count的值放入ecx寄存器 

类似的还有: 

a eax 

b ebx 

c ecx 

d edx 

S esi 

D edi 

I 常数值,(0 - 31) 

q,r 动态分配的寄存器 

g eax,ebx,ecx,edx或内存变量 

A 把eax和edx合成一个64位的寄存器(use long longs) 

我们也可以让gcc自己选择合适的寄存器. 

如下面的例子: 

asm("leal (%1,%1,4),%0" 

:  "=r" (x) 

:  "0" (x) ); 

这段代码实现5*x的快速乘法. 

得到的主要汇编代码为: 

movl x,%eax 

#APP 

leal (%eax,%eax,4),%eax 

#NO_APP 

movl %eax,x 

几点说明: 

1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器. 

使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器. 

2.我们不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器 

已经记住了它们. 

3."="是标示输出寄存器,必须这样用. 

4.数字%n的用法: 

数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求 

的寄存器.如果我们要重用"r"或"q"请求的寄存器的话,就可以使用它们. 

5.如果强制使用固定的寄存器的话,如不用%1,而用ebx,则 

asm("leal (%%ebx,%%ebx,4),%0" 

:  "=r" (x) 

:  "0" (x) ); 

注意要使用两个%,因为一个%的语法已经被%n用掉了. 

下面可以来解释letter 4854-4855的问题: 

1、变量加下划线和双下划线有什么特殊含义吗? 

加下划线是指全局变量,但我的gcc中加不加都无所谓. 

2、以上定义用如下调用时展开会是什么意思? 

#define _syscall1(type,name,type1,arg1) \ 

type name(type1 arg1) \ 

{ \ 

long __res; \ 

/* __res应该是一个全局变量 */ 

__asm__ volatile ("int $0x80" \ 

/* volatile 的意思是不允许优化,使编译器严格按照你的汇编代码汇编*/ 

:  "=a" (__res) \ 

/* 产生代码 movl %eax, __res */ 

:  "0" (__NR_##name),"b" ((long)(arg1))); \ 

/* 如果我没记错的话,这里##指的是两次宏展开. 

  即用实际的系统调用名字代替"name",然后再把__NR_...展开. 

  接着把展开的常数放入eax,把arg1放入ebx */ 

if (__res >= 0) \ 

return (type) __res; \ 

errno = -__res; \ 

return -1; \ 


////////////////////////////////////////////////////////////////////////

四.AT&T汇编与Intel汇编的比较

Intel和AT&T语法的区别
Intel和AT&T汇编语言的语法表面上各不相同,这将导致刚刚学会INTEL汇编的人第一次见到AT&T汇编时
会感到困惑,或者反之。因此让我们从基础的东西开始。

前缀
在Intel汇编中没有寄存器前缀或者立即数前缀。而在AT&T汇编中寄存器有一个“%”前缀,立即数有
一个“$”前缀。Intel语句中十六进制和二进制数据分别带有“h”和“b”后缀,并且如果十六进制
数字的第一位是字母的话,那么数值的前面要加一个“0”前缀。
例如,
Intex Syntax
mov    eax,1
mov    ebx,0ffh
int    80h

AT&T Syntax
movl    $1,%eax
movl    $0xff,%ebx
int     $0x80
就像你看到的,AT&T非常难懂。[base+index*scale+disp] 看起来比disp(base,index,scale)更好理解。
 

操作数的用法
intel语句中操作数的用法和AT&T中的用法相反。在Intel语句中,第一个操作数表示目的,第二个
操作数表示源。然而在AT&T语句中第一个操作数表示源而第二个操作数表示目的。在这种情形下AT&T语法
的好处是显而易见的。我们从左向右读,也从左向右写,这样比较自然。
例如,
Intex Syntax
instr    dest,source
mov    eax,[ecx]
    
AT&T Syntax
instr     source,dest
movl    (%ecx),%eax

存储器操作数
如同上面所看到的,存储器操作数的用法也不相同。在Intel语句中基址寄存器用“[”和“]”括起来
而在AT&T语句中是用“(”和“)”括起来的。
例如,
Intex Syntax
mov    eax,[ebx]
mov    eax,[ebx+3]
AT&T Syntax
movl    (%ebx),%eax
movl    3(%ebx),%eax
AT&T语法中用来处理复杂的操作的指令的形式和Intel语法中的形式比较起来要难懂得多。在Intel语句
中这样的形式是segreg:[base+index*scale+disp]。在AT&T语句中这样的形式是
%segreg:disp(base,index,scale)。
Index/scale/disp/segreg 都是可选并且可以去掉的。Scale在本身没有说明而index已指定的情况下
缺省值为1。segreg的确定依赖于指令本身以及程序运行在实模式还是pmode。在实模式下它依赖于
指令本身而pmode模式下它是不需要的。在AT&T语句中用作scale/disp的立即数不要加“$”前缀。
例如
Intel Syntax
instr     foo,segreg:[base+index*scale+disp]
mov    eax,[ebx+20h]
add    eax,[ebx+ecx*2h]
lea    eax,[ebx+ecx]
sub    eax,[ebx+ecx*4h-20h]    
AT&T Syntax
instr    %segreg:disp(base,index,scale),foo
movl    0x20(%ebx),%eax
addl    (%ebx,%ecx,0x2),%eax
leal    (%ebx,%ecx),%eax
subl    -0x20(%ebx,%ecx,0x4),%eax

后缀
就像你已经注意到的,AT&T语法中有一个后缀,它的意义是表示操作数的大小。“l”代表long,
“w”代表word,“b”代表byte。Intel语法中在处理存储器操作数时也有类似的表示,
如byte ptr, word ptr, dword ptr。"dword" 显然对应于“long”。这有点类似于C语言中定义的
类型,但是既然使用的寄存器的大小对应着假定的数据类型,这样就显得不必要了。
例子:
Intel Syntax
mov    al,bl
mov    ax,bx
mov    eax,ebx
mov    eax, dword ptr [ebx]    
AT&T Syntax
movb    %bl,%al
movw    %bx,%ax
movl    %ebx,%eax
movl    (%ebx),%eax

注意:从此开始所有的例子都使用AT&T语法
系统调用
本节将介绍linux中汇编语言系统调用的用法。系统调用包括位于/usr/man/man2的手册里第二部分所有
的函数。这些函数也在/usr/include/sys/syscall.h中列出来了。一个重要的关于这些函数的列表是
http://www.linuxassembly.org/syscall.html里。这些函数通过linux中断服务:int $0x80来被执行
小于六个参数的系统调用
对于所有的系统调用,系统调用号在%eax中。对于小于六个参数的系统调用,参数依次存放
在%ebx,%ecx,%edx,%esi,%edi中,系统调用的返回值保存在%eax中。
系统调用号可以在/usr/include/sys/syscall.h中找到。宏被定义成SYS_的形式,
如SYS_exit, SYS_close等。
例子:(hello world 程序)
参照write(2)的帮助手册,写操作被声明为ssize_t write(int fd, const void *buf, size_t count);
这样,fd应存放在%ebx中,buf放在 %ecx, count 放在 %edx , SYS_write 放在 %eax中,紧跟着是
int $0x80语句来执行系统调用。系统调用的返回值保存在%eax中。
$ cat write.s
.include "defines.h"
.data
hello:
    .string "hello world\n"

.globl    main
main:
    movl    $SYS_write,%eax
    movl    $STDOUT,%ebx
    movl    $hello,%ecx
    movl    $12,%edx
    int    $0x80

    ret
$
少于5个参数的系统调用的处理也是这样的。只是没有用到的寄存器保持不变罢了。象open或者fcntl这样
带有一个可选的额外参数的系统调用也就知道怎么用了。
大于5个参数的系统调用
参数个数大于五个的系统调用仍然把系统调用号保存在%eax中,但是参数存放在内存中,并且指向第一个
参数的指针保存在%ebx中。
如果你使用栈,参数必须被逆序压进栈里,即按最后一个参数到第一个参数的顺序。然后将栈的指针拷贝
到%ebx中。或者将参数拷贝到一块分配的内存区域,然后把第一个参数的地址保存在%ebx中。
例子:(使用mmap作为系统调用的例子)。在C中使用mmap():
#i nclude
#i nclude
#i nclude
#i nclude
#i nclude

#define STDOUT    1

void main(void) {
    char file[]="mmap.s";
    char *mappedptr;
    int fd,filelen;

    fd=fopen(file, O_RDONLY);
    filelen=lseek(fd,0,SEEK_END);
    mappedptr=mmap(NULL,filelen,PROT_READ,MAP_SHARED,fd,0);
    write(STDOUT, mappedptr, filelen);
    munmap(mappedptr, filelen);
    close(fd);
}
mmap()参数在内存中的排列:
%esp    %esp+4    %esp+8    %esp+12    %esp+16    %esp+20
00000000    filelen    00000001    00000001    fd    00000000
等价的汇编程序:
$ cat mmap.s
.include "defines.h"

.data
file:
    .string "mmap.s"
fd:
    .long     0
filelen:
    .long     0
mappedptr:
    .long     0

.globl main
main:
    push    %ebp
    movl    %esp,%ebp
    subl    $24,%esp

//    open($file, $O_RDONLY);

    movl    $fd,%ebx    // save fd
    movl    %eax,(%ebx)

//    lseek($fd,0,$SEEK_END);

    movl    $filelen,%ebx    // save file length
    movl    %eax,(%ebx)

    xorl    %edx,%edx

//    mmap(NULL,$filelen,PROT_READ,MAP_SHARED,$fd,0);
    movl    %edx,(%esp)
    movl    %eax,4(%esp)    // file length still in %eax
    movl    $PROT_READ,8(%esp)
    movl    $MAP_SHARED,12(%esp)
    movl    $fd,%ebx    // load file descriptor
    movl    (%ebx),%eax
    movl    %eax,16(%esp)
    movl    %edx,20(%esp)
    movl    $SYS_mmap,%eax
    movl    %esp,%ebx
    int    $0x80

    movl    $mappedptr,%ebx    // save ptr
    movl    %eax,(%ebx)
        
//     write($stdout, $mappedptr, $filelen);
//    munmap($mappedptr, $filelen);
//    close($fd);
    
    movl    %ebp,%esp
    popl    %ebp

    ret
$
注意:上面所列出的源代码和本文结束部分的例子的源代码不同。上面列出的代码中没有说明其它的
系统调用,因为这不是本节的重点,上面列出的源代码仅仅打开mmap.s文件,而例子的源代码要读
命令行的参数。这个mmap的例子还用到lseek来获取文件大小。
Socket系统调用
Socket系统调用使用唯一的系统调用号:SYS_socketcall,它保存在%eax中。Socket函数是通过位于
/usr/include/linux/net.h的一个子函数号来确定的,并且它们被保存在%ebx中。指向系统调用参数
的一个指针存放在%ecx中。Socket系统调用也是通过int $0x80来执行的。
$ cat socket.s
.include "defines.h"

.globl    _start
_start:
    pushl    %ebp
    movl    %esp,%ebp
    sub    $12,%esp

//    socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    movl    $AF_INET,(%esp)
    movl    $SOCK_STREAM,4(%esp)
    movl    $IPPROTO_TCP,8(%esp)

    movl    $SYS_socketcall,%eax
    movl    $SYS_socketcall_socket,%ebx
    movl    %esp,%ecx
    int    $0x80

    movl     $SYS_exit,%eax
    xorl     %ebx,%ebx
    int     $0x80

    movl    %ebp,%esp
    popl    %ebp
    ret
$

命令行参数
在linux中执行的时候命令行参数是放在栈上的。先是argc,跟着是一个由指向命令行中各字符串的
指针组成的数组(**argv)并以空指针结束。接下来是一个由指向环境变量的指针组成的
数组(**envp)。这些东西在asm中都可以很容易的获得,并且在例子代码(args.s)中有示范。

 
GCC内联汇编
本节中GCC内联汇编仅涉及x86的应用程序。操作数约束会和其它处理器上的有所不同。关于这部分
的说明放在本文的最后。
gcc中基本的内联汇编非常易懂,如
__asm__("movl    %esp,%eax");    // look familiar ?

或者是
__asm__("
            movl    $1,%eax        // SYS_exit
            xor    %ebx,%ebx
            int    $0x80
    ");
如果指定了用作asm的输入、输出数据并指出哪一个寄存器会被修改,会使程序的执行效率提高。
input/output/modify都不是必需的。格式如下:
__asm__("" : output : input : modify);
output和input中必须包含一个操作数约束字符串,并紧跟一个用圆括号括起来的C语言表达式。
输出操作数约束的前面必须有一个“=”,表示这是一个输出。可能会有多个输出,多个输入和
多个修改过的寄存器。每个“入口”应该用“,”分隔开,并且入口的总数不多有10个。
操作数约束字符串可以是包含整个寄存器的名称也可以是简写。
Abbrev Table
Abbrev    Register
a    %eax/%ax/%al
b    %ebx/%bx/%bl
c    %ecx/%cx/%cl
d    %edx/%dx/%dl
S    %esi/%si
D    %edi/%di
m    memory
例如:

    __asm__("test    %%eax,%%eax", : /* no output */ : "a"(foo));


或者是

    __asm__("test    %%eax,%%eax", : /* no output */ : "eax"(foo));
你可以在__asm__后使用关键字__volatile__:“你可以利用在__asm__后使用关键字__volatile__的
方法防止一条‘asm’指令被删除、移动或者被重新组合。”(出自gcc的info文件中"Assembler
Instructions with C Expression Operands" 部分)
$ cat inline1.c
#i nclude

int main(void) {
    int foo=10,bar=15;
    
    __asm__ __volatile__ ("addl     %%ebxx,%%eax"
        : "=eax"(foo)         // ouput
        : "eax"(foo), "ebx"(bar)// input
        : "eax"            // modify
    );
    printf("foo+bar=%d\n", foo);
    return 0;
}
$
你可能已经注意到现在寄存器使用“%%”前缀而不是“%”。这在使用output/input/modify域时是必要的,
这是因为此时基于其它域的寄存器的别名的使用。我马上来讨论这个问题。
你可以很简单的指定“a”而不是写“eax”或者强制使用一个特殊寄存器如"eax"、"ax"、"al",
这同样适用于其它一般用途的寄存器(在Abbrev表中列出的)。当你在当前的代码中使用特殊的寄存器
时这好像毫无用处,因此gcc提供了寄存器别名。最多有10个别名(%0—%9),这也是为什么只允许10个
输入/输出的原因。
$ cat inline2.c
int main(void) {
    long eax;
    short bx;
    char cl;

    __asm__("nop;nop;nop"); // to separate inline asm from the rest of
                // the code
    __volatile__ __asm__("
        test    %0,%0
        test    %1,%1
        test    %2,%2"
        : /* no outputs */
        : "a"((long)eax), "b"((short)bx), "c"((char)cl)
    );
    __asm__("nop;nop;nop");
    return 0;
}
$ gcc -o inline2 inline2.c
$ gdb ./inline2
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnulibc1"...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
... start: inline asm ...
0x8048427 : nop
0x8048428 : nop
0x8048429 : nop
0x804842a : mov 0xfffffffc(%ebp),%eax
0x804842d : mov 0xfffffffa(%ebp),%bx
0x8048431 : mov 0xfffffff9(%ebp),%cl
0x8048434 : test %eax,%eax
0x8048436 : test %bx,%bx
0x8048439 : test %cl,%cl
0x804843b : nop
0x804843c : nop
0x804843d : nop
... end: inline asm ...
End of assembler dump.
$
就像你看到的,由内联汇编生成的代码将变量的值放入它们在input域中指定的寄存器中,然后继续
执行当前的代码。编译器自动根据变量的大小来侦测操作数的大小,这样相应的寄存器就被
别名%0, %1 和 %2代替了(当使用寄存器别名时在存储器里指定操作数的大小回导致编译时发生错误)
在操作数约束里也可以使用别名。这不允许你在输入/输出域中指定多于10个的入口。我能想到的这样
做的唯一用法是在你指定操作数约束为“q”以便让编译器在a,b,c,d寄存器之间进行选择的时候。
当这个寄存器被修改时,我们不会知道选中了那个寄存器,因而不能在modify域中指定它。
这种情况下你只需指定""。
例子:
$ cat inline3.c
#i nclude

int main(void) {
    long eax=1,ebx=2;

    __asm__ __volatile__ ("add %0,%2"
        : "=b"((long)ebx)
        : "a"((long)eax), "q"(ebx)
        : "2"
    );
    printf("ebx=%x\n", ebx);
    return 0;
}

一句话shell

下载连续的文件

for num in `seq -w  1 1 21`; do wget http://thns.tsinghua.edu.cn/thnsebooks/ebook73/$num.pdf; done

在当前目录下(排除0618和Mail目录)查找以a开头的文件或目录

find . \( -path './0618*' -o -path './Mail*' \) -a -prune -o -name "a*" -print

在当前目录的c或h文件中查找字符串

find . "*.[ch]" -exec grep -nH "main" {} \;

Ubuntu下列出system-config-printer-gnome包所安装的文件占的空间

du -cxh $(file `dpkg -L system-config-printer-gnome`|grep -v directory|awk -F: '{print $1}')

gdb的最简单用法

1.在编译时加入参数 ”-g” 使编译出来的程序带debug信息

$>gcc test.c –g –o test

2. gdb运行程序

a)         运行gdb

$>gdb test

b)        运行程序,带参数

(gdb) run arg1 arg2 arg3

3.连接到一个正在执行的程序

a)         看该程序的进程号

$>ps –aux

b)        运行gdb

$>gdb

c)        连接gdb与该进程

(gdb) attach 进程号

d)        此时程序处于中断状态,让程序往下执行

(gdb) continue

4.设置断点

a)         设置断点

(gdb) break 函数名

(gdb) break 46 if testsize==100  如果变量testsize等于100,则在46行停

b)        执行到断点后程序就停下来了,此时让程序执行下一行

                         i.              执行下一行,如遇函数跳过

(gdb) next

                       ii.              执行下一行,如遇函数进入

(gdb) step

                      iii.              如果在step时进入了函数,执行到一半想跳过函数

              (gdb) finish

c)        让程序继续往下执行 

(gdb) continue

d)        删除断点

                         i.              删除某一断点

(gdb) delete breakpoint 断点号

                       ii.              删除所有断点

(gdb) clear

5.看变量状态

a)         显示变量值

(gdb) print 变量名

b)        显示变量类型

(gdb) whatis 变量名

6.看程序中的当前位置(在程序死了的时候看容易看到是怎么死的)

(gdb) backtrace (gdb) where

7.看相应程序代码

按键Ctrl+x, 再铵a, 则可以看到当前执行的程序所对应的代码,再按一遍切回去

glade和libglade

CFLAGS = $(shell pkg-config --cflags gtk+-2.0 libglade-2.0)
LDFLAGS = $(shell pkg-config --libs gtk+-2.0 libglade-2.0)

main: main.o

.c.o:
    $(CC) -g $(CFLAGS) -c $< -o $@

clean:
    rm -f *.o main
这样就很容易产生可执行文件了.

下面是main.c的原文件内容
/***************************************************************************
 *       main.c
 *
 *  Wed Aug 17 12:42:58 2005
 *  Copyright  2005  YangH , GNOME-cn
 *  http://www.gnome-cn.org
 ****************************************************************************/




#include <stdio.h>

#include <gtk/gtk.h>
#include <glade/glade.h>

#define GLADE_FILE "./glade-custom-widget-demo.glade"

GtkWidget* custom_widget_create (gchar *widget_name, gchar *string1, gchar *string2,
                               gint int1, gint int2);

int main (int argc, char **argv)
{
    GtkWidget *window;
    GladeXML *xml;

    gtk_init (&argc, &argv);

    xml = glade_xml_new (GLADE_FILE, NULL, NULL);
    glade_xml_signal_autoconnect (xml);
   
    gtk_main ();

    return 0;
}

GtkWidget* custom_widget_create (gchar *widget_name, gchar *string1, gchar *string2,
                               gint int1, gint int2)
{
    GtkWidget *widget;
    GtkWidget *box, *button;

    box = gtk_vbox_new (FALSE, 4);
    button = gtk_button_new_with_label(g_strdup_printf ("Name: %s", widget_name));
    gtk_box_pack_start (GTK_BOX(box), button, FALSE, FALSE, 4);

    widget = gtk_label_new (g_strdup_printf ("string1: %s", string1));
    gtk_box_pack_start (GTK_BOX(box), widget, FALSE, FALSE, 4);
    widget = gtk_label_new (g_strdup_printf ("string2: %s", string2));
    gtk_box_pack_start (GTK_BOX(box), widget, FALSE, FALSE, 4);

    /* This very important */
    gtk_widget_show_all (box);
   
    return box;
}

shell中变量的自增

#!/usr/local/bin/bash
i=0;
while [ $i -lt 4 ]; do
 

   echo $i;
    i=$[$i+1];
done

VIM设置代码折叠

在查看代码的时候,如果代码十分冗长,尤其是c/c++系列的时候,
代码结构比较让人难以理清,主要是各种注释、括号的匹配问题,
虽然可以用%来查看匹配的另外一半符号在哪里,但是如果这一对符号之间的代码超过了一页,甚至好几页的话,那么也是比较难以理解的。
于是可以使用这个vim所提供的折叠功能来简化这些分析。
我使用的vim是rh9自带的版本。
折叠的方式:

vim 提供 6中折叠方式
          manual           手工定义折叠
          indent             更多的缩进表示更高级别的折叠
          expr                用表达式来定义折叠
          syntax             用语法高亮来定义折叠
          diff                  对没有更改的文本进行折叠
          marker            对文中的标志折叠


可用选项 'foldmethod' 来设定折叠方式:set fdm=*****。
注意,每一种折叠方式不兼容,即:你不能用expr又用marker方式。
目前我一般使用的都是indent 比较多。
使用时,用:set fdm=indent 命令来设置成marker折叠方式。
在.vimrc文件中添加设置,可以使得每次打开vi 都启动折叠方式。如添加:set fdm=syntax。

2. 折叠打开与折合
选取了折叠方式后,我们就可以对某些代码实施我们需要的折叠了。

如果使用了indent方式,vim会自动的对大括号的中间部分进行折叠,我们可以直接使用这些现成的折叠成果。
indent 对应的折叠代码有:
zc      折叠
zC     对所在范围内所有嵌套的折叠点进行折叠
zo      展开折叠
zO     对所在范围内所有嵌套的折叠点展开
[z       到当前打开的折叠的开始处。
]z       到当前打开的折叠的末尾处。
zj       向下移动。到达下一个折叠的开始处。关闭的折叠也被计入。
zk      向上移动到前一折叠的结束处。关闭的折叠也被计入。

使用时在大括号中间输入以上命令。

当使用marker方式时,需要用标计来标识代码的折叠,系统默认是{{{和}}},最好不要改动之:)
可以使用下面的命令来创建和删除折叠:

zf      创建折叠,比如在marker方式下:
                   zf56G,创建从当前行起到56行的代码折叠;
                   10zf或10zf+或zf10↓,创建从当前行起到后10行的代码折叠。
                   10zf-或zf10↑,创建从当前行起到之前10行的代码折叠。
                   在括号处zf%,创建从当前行起到对应的匹配的括号上去((),{},[],<>等)。
zd      删除 (delete) 在光标下的折叠。仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。
zD     循环删除 (Delete) 光标下的折叠,即嵌套删除折叠。
          仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。
zE     除去 (Eliminate) 窗口里“所有”的折叠。
          仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。

Portage用户使用指南

Debentoo Gao译 2002.12.16



1 初步认识


 取得最新的Portage软件包列表



在你安装了Gentoo Linux,使用了一段时间以后,发现一些软件有着这样那样的bugs,觉得不满意,或是想升级到最新的Gentoo Linux所带的软件包时,你需要下载我们的Portage树。我提供了一些匿名的rsync服务器以供您升级到最新的Portage树。下面我将告诉您如何使用它。



使用以下命令同步更新你的Portage树


#emerge rsync



请注意emerge rsync;命令会自动附带—clean参数,清除你在/usr/portage里做的个人设置。如果你想保存你的主Portage树个人设置不变的话,请使用PORTDIR_OVERLAY选项:



把下面一行加入/etc/make.conf


PORTDIR_OVERLAY="/dir/where/your/ebuilds/are"



如果你已经动手修改ebuilds,修正bugs。你可以考虑加入Gentoo Linux开发团队,成为我们的一员。具体事项可以和Daniel Robbins或Seemant Kulleen联系。



升级Portage


 在使用我们的Portage树前,升级它是一件很重要的工作。你可以这么做:


#emerge -up system *它会告诉你要升级哪些包


#emerge -u system *开始升级你的所需要的包


现在你的Portage升级到了最新版本,接下去你就可以随心所欲用我们的ebuild系统升级你所需的软件了。



2 emerge命令的介绍



emerge ?pretend


在安装一个软件包前,看看它和其他包的倚赖关系或哪些包会被升级是个不错的主意。你可以使用emerge ?pretend或emerge -p命令来察看。



比如说:# emerge -p xchat


These are the packages that I would merge, in order.



Calculating dependencies......... done!


[ebuild U] sys-libs/zlib-1.1.3-r2 to /


[ebuild U] dev-libs/glib-1.2.10 to /


[ebuild N ] media-libs/jpeg-6b-r2 to /


[ebuild N ] x11-base/xfree-4.0.3-r3 to /


[ebuild N ] x11-libs/gtk+-1.2.10-r1 to /


[ebuild N ] media-libs/giflib-4.1.0-r3 to /


[ebuild N ] media-libs/tiff-3.5.6_beta to /


[ebuild N ] media-libs/imlib-1.9.10 to /


[ebuild N ] net-irc/xchat-1.4.3 to /



如上所示,我们试着在一台没有安装X的机器上emerge xchat.emerge ?pretend正确的列出了所必需满足的倚赖关系。并明确的指出了sys-libs/zlib和dev-libs/glib需要升级,emerge xchat时,这些所倚赖的包(当然包括x11-base/xfree会被一同安装。



USE和emerge


以上,我在一个/etc/make.conf文件USE环境变量没有定义gnome的系统上执行了emerge ?pretend命令。就是说GNOME支持是可选项,一般是不打开的。那既然有这个选项,那我们就试着打开它,在执行emerge ?pretend命令看看输出有什么不同。


# emerge -p xchat



These are the packages that I would merge, in order.



Calculating dependencies............................ done!


[ebuild N ] media-libs/jpeg-6b-r2 to /


[ebuild N ] gnome-base/libghttp-1.0.9 to /


[ebuild N ] media-libs/audiofile-0.2.1 to /


[ebuild N ] media-sound/esound-0.2.22-r2 to /


[ebuild N ] gnome-base/gnome-env-1.0 to /


[ebuild N ] gnome-base/libxml-1.8.11 to /


[ebuild N ] gnome-base/ORBit-0.5.8 to /


[ebuild N ] gnome-base/oaf-0.6.5 to /


[ebuild U] dev-libs/glib-1.2.10 to /


[ebuild N ] net-libs/libwww-5.3.2-r1 to /


[ebuild N ] media-libs/giflib-4.1.0-r3 to /


[ebuild N ] dev-util/guile-1.4-r3 to /


[ebuild U] sys-libs/zlib-1.1.3-r2 to /


[ebuild N ] x11-base/xfree-4.0.3-r3 to /


[ebuild N ] x11-libs/gtk+-1.2.10-r1 to /


[ebuild N ] media-libs/tiff-3.5.6_beta to /


[ebuild N ] media-libs/imlib-1.9.10 to /


[ebuild N ] gnome-base/gnome-libs-1.2.13 to /


[ebuild N ] gnome-base/glibwww-0.2-r1 to /


[ebuild N ] gnome-base/gdk-pixbuf-0.11.0 to /


[ebuild N ] gnome-base/gconf-1.0.0 to /


[ebuild N ] gnome-base/gnome-vfs-1.0.1 to /


[ebuild N ] gnome-base/control-center-1.4.0.1 to /


[ebuild N ] gnome-base/scrollkeeper-0.2 to /


[ebuild N ] dev-util/xml-i18n-tools-0.8.1 to /


[ebuild N ] gnome-base/libglade-0.16-r1 to /


[ebuild N ] gnome-base/gnome-core-1.4.0.4 to /


[ebuild N ] net-irc/xchat-1.4.3 to /



很清楚地,在USE变量中加入gnome,emerge很快的知道安装xchat要加入对gnome的支持。当然为了GNOME选项的正确编译和运行, GNOME必需被首先安装,emerge会计算GNOME 安装所倚赖的包,并从它的ebuild列表中加入。当USE变量设置错误的时候,emerge命令很可能无法正确执行。那也是我们为什么建议在实际 emerge软件包,特别是比较新的,自己不熟悉的ebuild前先用emerge ?pretend命令察看一下的原因。接下去,你该知道怎么做了吧:)。 一切顺利的话,你可以撇开--pretend选项emerge了。


#emerge xchat



等满足了所有的倚赖关系(如果这种关系存在,不是所有的包都有这种倚赖关系。),接着xchat源码包会被下载(存储到 /usr/portage/distfiles目录),并在一个叫sandbox的零时目录下校验MD5,解压,编译和安装。接着他们被合并到本地文件系统,在/var/db/pkg/net-irc/xchat-1.4.3/CONTENTS目录下建立该包的数据库,包括所有安装的包和它们的 md5sum。



3 升级软件包


一般的我们升级一个软件包所用到的命令包括 emerge --update or emerge -u。



# emerge -u xchat


Portage使用一个被称为“Safe”的umerge命令负责拆卸原来的文件。如果一个文件已经被手动复盖或改写,它会先被从系统中移出,(也许,你已经安装了新版本的软件。)。如此一来,当你merge了新版本的xchat后umerge一个旧的版本,xhchat可执行文件不会被你的系统删除,因为它已有了新的时间信息和不同的md5sum。Safe unmerges 真得很棒,它可以在任何时候确认可用的软件版本。如果你先执行了unmerge命令,那么直到你下载安装了新版本的xchat,它才可被继续使用。



重点:


现在Portage加入了被称为“配置文件保护”的新特性。设计这个新特性的目的是为了防止新装的软件破坏原有的配置文件。一般的,配置文件保护特性默认在/etc和KDE配置目录下打开,将来会加入到更多其他的配置目录中。详情请执行emerge --help config命令察看。