table th:first-of-type {width: 80px; }
table th:nth-of-type(2) {width: 249px; }
table th:nth-of-type(3) {width: 249px; }
开发过程涉及的一些基础知识
编译器和编译工具
- gcc:GUN c语言编译器,支持C99标准并拥有独特的扩展
- as:GAS汇编语言编译器,用于编译AT&T格式的汇编语言
- ld:链接器,用于将编译文件链接成可执行文件
- nasm:NASM汇编语言编译器,用于编译Intel格式的汇编语言
- make:编译工具,根据编译脚本文件记录的内容编译程序
系统工具与命令
- dd:复制制定大小的数据块,并在复制过程中转换数据格式
- mount: 挂在命令,用于将U盘等存储设备挂在到指定路径上
- umount:卸载命令,与mount命令功能相反
- cp:复制命令
- sync:数据同步命令,将已缓存的数据回写到存储设备上
- rm:删除命令
- objdump:反汇编命令,负责将可执行文件反编译成汇编语言
- objcopy:文件提取命令,将源文件中的内容提取出来,再转存到目标文件中
Bochs相关的调试命令
| 指令 | 说明 | 举例 | |
|---|---|---|---|
| b address | 在某物理地址上设置断点 | b 0x7c00 | |
| c | 继续执行,知道遇到断点 | c | |
| s | 单步执行 | s | |
| info cpu | 查看寄存器信息 | info cpu | |
| r | 查看寄存器信息 | r | |
| sreg | 查看寄存器信息 | sreg | |
| creg | 查看寄存器信息 | creg | |
| xp /nuf addr | 查看内存地址内容 | xp /10bx 0x100000 | |
| x | /nuf addr | 查看线性地址内容 | x /40wd 0x9000 |
| u start end | 反汇编一段内存 | u 0x100000 ox100010 | |
| 注: n代表显示单元个数;u代表显示单元大小[b: Byte, h:Word, w: DWord, g: QWord(4 字)];f代表显示格式(x: 16进制, d:10进制, t:2进制, c:字符) |
AT&T汇编与Intel汇编格式对比表
| 项目 | Intel汇编格式 | AT&T汇编格式 |
|---|---|---|
| 书写格式 | 关键字大写,如:MOV AX, 0x10 | 关键字小写,如:mov %0x10, %ax |
| 赋值方向 | 从右向左,如:ADD 目的, 源 | 从左向右 |
| 操作数前缀 | 使用寄存器和立即数无需额外添加前缀,如:MOV CX,12 | 使用寄存器必须在前面添加指令前缀%,使用立即数必须在前面添加前缀$,例如:mov $12, %cx 对于标识符变量,可以直接引用,无需添加前缀。例如: values: .long 0x5a5a5a5a movl values, %eax 此处的values是一个标识符变量,这条指令的意思是将values变量记录的0x5a5a5a5a装入寄存器eax中。 如果添加标识符前缀$,则说明正在引用该变量的地址。例如: movl $values, %ebx 这条汇编指令的意思是将values变量的地址装入ebx寄存器 |
| 跳转和调用指令 | 远跳转指令JMP的目标地址由段地址和段内偏移组成。远调用指令CALL的目标地址同样由段地址和段内偏移组成,远返回指令RET无操作数: CALL FAR SECTION:OFFSET JMP FAR SECTION:OFFSET RET | 对于远跳转指令和远调用指令必须使用前缀l加以修饰,与lcall指令相对应的是远返回指令lret。例如: lcall $section:$offset ljmp $section:$offset lret |
| 内存间接寻址格式 | Intel使用[ ]来表示间接寻址,完成格式为section:[base+index*scale+displacement]其中scale的默认值为1,可取值是1、2、4、8;section用于指定段寄存器,不同情况的默认段寄存器是不同的 | AT&T使用( )来表示间接寻址,格式为section:displacement(base, index, scale),这里的section, base, index, scale, displacement与Intel的使用规则相同 |
| 指令的后缀 | 使用内存操作数时,应该对操作数的位宽加以限定,借助修饰符PTR可以限定操作数的位宽,例如:BYTE PTR代表一个字节、WORD PTR代表一个字、DWORD PTR代表一个双字等。例如: MOV EAX, DWORD PTF [EBX] | AT&T语法中大部分指令在访问数据时都需要指明操作数的位宽,通常一个字节用b表示、一个字用w表示、一个双子用l表示、一个四字用q表示。例如: movq %rax, %rbx 此外,跳转指令的地址标识符也可添加后缀以表示跳转方向,f表示向前跳转(forward),b表示向后跳转(back),如: jmp lf l: |
NASM编译器
- 符号[]
- 在NASM编译器中,如果直接引用变量名或者标识符,则被编译器认为正在引用该变量的地址
- 如果希望访问变量内的数据,则必须使用符号[]
- 符号$
- 符号$在NASM编译器中代表当前行被编译后的地址。
- 符号?
- 代表一个Section(节)起始处被编译后的地址,也就是这个节的其实地址
- $-?,表示本行程序距离Section起始处的偏移,如果只有一个节,它便表示本行程序距离程序起始处的距离
函数调用约定
- stdcall调用约定
- 在调用函数时,参数将按照从右向左的顺序依次入栈。
- 函数的栈平衡操作(参数出栈操作)是由被调用函数完成的。通过代码retn x可在函数返回时从栈上弹出x字节的数据。当CPU执行RET指令是,会将栈指针寄存器ESP向上移动x个字节,来模拟栈的弹出操作。
- 函数编译过程中,编译器会在函数名前用下划线修饰,其后用符号@修饰,并加上入栈的字节数,因此int function(int first, int second)会被编译成_function@8
- cdecl调用约定
- cdecl调用约定的参数压栈顺序与stdcall相同。
- 函数的栈平衡操作使用调用函数完成的,这点与stdcall恰恰相反。stdcall调用约定使用代码retn x平衡栈,而cdecl调用约定则通常会借助代码leave、pop或向上移动栈指针等方法来平衡栈。
- 每个函数调用者都含有平衡栈的代码,因此编译生成的可执行文件会比stdcall调用约定生成的文件大。
- cdecl是GUN C编译器的默认调用约定。但GUN C在64位系统环境下,却使用寄存器作为函数参数的传递方式。函数调用者按照从左向右的顺序依次将前6个整形参数放在通用寄存器RDI、RSI、RDX、RCX、R8和R9中;同时,寄存器XMM0~XMM7用来保存浮点变量,而RAX寄存器则用来保存函数的返回值,函数调用者负责平衡栈。
参数传递方式
- 寄存器传递方式
- 内存传递方式
GUN C内嵌汇编语言
`#define nop() __asm__ __volatile__ ("nop \n\t")`
- __asm__关键字:用于声明这行代码是一个内嵌汇编表达式,它是关键字asm的宏定义(
#define __asm__ asm)。 - __volatile__关键字:告诉编译器此行代码不能被编译器优化,编译时代持代码原状。
-
内嵌汇编表达式
`指令部分:输出部分:输入部分:损坏部分`
- 指令部分是汇编代码本身,指令部分是内嵌汇编表达式的必填项,而其他部分可以省略。指令部分的编写规则要求是:当指令表达式中存在多条汇编代码时,可全部书写在一对双引号中;亦可将汇编代码放在多对双引号中。如果将所有指令编写在同一双引号中,那么相邻两条指令间必须使用分号(;)或换行符(\n)分隔。如果使用换行符,通常在其后还会紧跟一个制表符(\t)。当汇编代码引用寄存器时,必须在寄存器名前面再添加一个%符,以表示对寄存器的引用,例如代码"
movl $0x10, %%eax"。 - 输出部分
- 格式:(“输出操作约束符”(输出表达式······))
- 约束部分必须使用“=”或“+”进行修饰,“=”意味着输出表达式时一个纯粹的输出操作,“+”意味着输出表达式既用于输出操作,有用于输入操作。只能用在输出部分,不能用在输入部分,而且时可读写的。
- 输入部分
格式:(“输入操作约束符”(输入表达式······)),输入部分时只读的。 - 损坏部分
- 操作约束和修饰符
每个输入/输出表达式都必须指定自身的操作约束。操作约束的类型可以细分为寄存器约束、内存约束和立即数约束。在输出表达式中,还有限定寄存器操作的修饰符。
- 寄存器约束限定了表达式的载体是一个寄存器,可以明确指派,亦可模糊指派再由编译器自行分配。可用全名,也可以使用缩写名称。如下所示:
__asm__ __volatile__ ("movl %0, %%cr0"::"eax"(cr0));
__asm__ __volatile__ ("movl %0, %%cr0"::"a"(cr0));
| 缩写 | 描述 |
|---|---|
| r | 任何输入/输出型的寄存器 |
| q | 从EAX / EBX / ECX / EDX中指派一个寄存器 |
| g | 寄存器或者内存空间 |
| m | 内存空间 |
| a | 使用RAX / EAX / AX / AL寄存器 |
| b | 使用RBX / EBX / BX / BL寄存器 |
| c | 使用RCX / ECX / CX / CL寄存器 |
| d | 使用RDX / EDX / DX / DL寄存器 |
| D | 使用RDI / EDI / DI寄存器 |
| S | 使用RSI / ESI / SI寄存器 |
| f | 选用浮点寄存器 |
| i | 一个整数类型的立即数 |
| F | 一个浮点类型的立即数 |
- 内存约束限定了表达式的载体是一个内存空间,使用约束名m表示。例如:
__asm__ __volatile__ ("sgdt %0":"=m"(__gdt_addr)::);
__asm__ __volatile__ ("lgdt %0"::"m"(__gdt_addr));
- 立即数约束只能用于输入部分,它限定了表达式的载体是一个数值。例如:
__asm__ __volatile__ ("movl %0, %%ebx"::"i"(50));
使用约束名i限定整数类型的立即数,F位浮点数。
- 修饰符只可用在输出部分,除了等号=和加号+外,还有&符。