程序的加载和执行(六)——《x86汇编语言:从实模式到保护模式》读书笔记26
通过本文能学到什么?
- NASM的条件汇编
- 用NASM编译的时候,通过命令行选项定义宏
- Makefile的条件语句
- 在make命令行中覆盖Makefile中的变量值
- 第13章习题解答
- 复习如何构造栈段描述符
我们接着上篇博文说。
在我修改后的文件中,用到了条件汇编。
比如:
%ifdef DEBUG
put_core_salt: ;打印内核的符号
...
...
put_usr_salt: ;打印用户的符号
...
...
%endif
下文对此进行讲解。
1.条件汇编
与C预处理器相似,NASM允许对一段源代码只在某特定条件满足时进行汇编。
注意,C语言的预处理指令是由#
字符开头的一些命令,NASM编译器的预处理指令由%
开头。
1.1 只在某特定条件满足时进行汇编
%if<condition>
;if <condition>满足时接下来的代码被汇编。
......
%elif<condition2>
; 当 if<condition>不满足,而<condition2>满足时,该段代码被汇编。
......
%else
;当<condition>跟<condition2>都不满足时,该段代码被汇编。
......
%endif
%else
跟%elif
子句都是可选的,也可以使用多于一个的%elif
子句。
1.2 测试单行宏是否存在
%ifdef DEBUG
...
%endif
如果我们定义了宏DEBUG
,那么省略号处的代码就会被汇编,否则不会。
以定义宏DEBUG
为例,可以用两种方法。
1.3 在代码中直接定义宏
%define DEBUG
1.4 通过命令行选项
编译文件的时候,用-d 宏名称
。
-d DEBUG
例如:
nasm c13_core.asm -o c13_core.bin -d DEBUG
注意:-d
可以写成-D
,它和后面宏名之间的空格也可以不要。
例如:
nasm c13_core.asm -o c13_core.bin -dDEBUG
2.关于Makefile
因为加入了条件汇编,所以Makefile和上篇博文中的不太一样。
上篇博文地址:
程序的加载和执行(五)——《x86汇编语言:从实模式到保护模式》读书笔记25
修改后的Makefile如下。
DEBUG = 0
BIN = c13_mbr.bin c13_core.bin c13.bin empty
A_DIR = /home/cjy/a.img
C_DIR = /home/cjy/c.img
all:$(BIN)
.PHONY:all clean
c13_mbr.bin:c13_mbr.asm
nasm $< -o $@
dd if=$@ of=$(A_DIR)
c13_core.bin:c13_core.asm
ifeq ($(DEBUG),1)
nasm $< -o $@ -D DEBUG
else
nasm $< -o $@
endif
dd if=$@ of=$(C_DIR) bs=512 seek=1 conv=notrunc
c13.bin:c13.asm
nasm $< -o $@
dd if=$@ of=$(C_DIR) bs=512 seek=50 conv=notrunc
empty:diskdata.txt
dd if=$< of=$(C_DIR) bs=512 seek=100 conv=notrunc
touch $@
clean:
$(RM) $(BIN)
2.1 条件语句
首先,我们定义了一个变量(也被称作宏)DEBUG
,并给其赋值为0;
需要注意的是以下几行:
ifeq ($(DEBUG),1)
nasm $< -o $@ -D DEBUG
else
nasm $< -o $@
endif
这里用到了Makefile的条件语句。执行make时,会根据运行时的不同情况选择不同的执行分支。
在这个例子中,当变量DEBUG
的值为1时,就执行nasm $< -o $@ -D DEBUG
;
当变量DEBUG
的值不为1时,就执行nasm $< -o $@
;
当我们想编译带调试信息的源文件,修改Makefile中DEBUG
的值为1就可以了。
但是,还有没有更简单的方法呢?
2.2 在命令行中定义变量
当命令行中的变量(宏)定义跟makefile中的定义有冲突时,以命令行中的定义为准。所以,我们可以在执行make的时候加上变量=新值
,以覆盖Makefile文件中的变量值。
对于本文的例子,除了修改Makefile中DEBUG
的值为1这种方法外,还有一种方法是在命令行中重新给变量(宏)赋值。
make "DEBUG=1"
注意:当没有空格的时候,引号也可以省略。由于宏定义必须作为单个参数进行传递,所以要避免使用空格,所以更妥当的方法是使用引号。
3.第13章习题解答
在本章中,用户程序只给出建议的栈大小,但并不提供栈空间。现在,修改内核程序和用户程序,改由用户程序自行提供栈空间。要求:栈段必须定义在用户程序头部之后。
在这里,我给出自己的答案,供学习者参考。
3.1 对于源文件c13.asm
的修改
修改有两处。第一处是:
由于栈空间由用户程序提供,所以必须指明栈段的汇编地址和大小,这样内核程序才能有足够的信息为用户创建栈段描述符。
第二处是:
因为题目已经要求栈段必须定义在用户程序头部之后,所以这里紧接着头部定义栈空间。
小插曲
编译这个文件,会报错。
c13.asm:14: error: division operator may only be applied to scalar values
make: *** [c13.bin] 错误 1
哦,这是为什么呢?我改成了
stack_len dd (stack_end-0)/(4*1024)
依然报同样的错误。
通过查资料,发现有的朋友是这样回答的:
A label is a relocatable value——its value is modified by the linker/loader.The difference between two labels(in the same section) is a scalar value, and NASM will work with it.
好吧,既然同一个section的两个标号的差值是标量,那我这样写好了:
stack_len dd (stack_end-stack_start)/(4*1024)
其实stack_start
这个标号之前是没有的,是我为了做差值专门加上的。
你还别说,这样写真的管用,不报错了。
3.2 对源文件c13_core.asm
的修改
左边的代码(配书代码),内核为用户栈分配内存,然后计算栈的高端物理地址。
右边的代码(习题代码),内核不需要分配内存,仅仅从头部取出栈的起始地址,然后计算栈的高端物理地址。
这段代码是我半年前写的,可是现在读起来也觉得陌生了。那就解释一下最关键的3行,加深自己的记忆。
469 mov edx,edi
470 add edx,[edi+0x08] ; 栈段起始的线性地址
471 add eax,edx ; 得到栈的高端物理地址
此时,DS指向了0-4GB的数据段,EDI中的内容是用户程序的加载地址。
469:EDX中是用户程序的加载地址;
470:从用户头部偏移0x08处取得section.stack.start
,加上用户程序的加载地址(EDX),就得到了用户栈的起始地址。如图所示。
471:栈段描述符中的基地址,应该是栈空间的高端物理地址,所以还要加上栈的大小(在EAX中)。
如果不明白为什么这样构造栈段描述符,可以参考我的博文:
如何构造栈段描述符
3.3 在Bochs中验证程序
我们的修改对吗?“实践出真知”。
3.3.1 验证思路
因为栈空间是用户程序提供的,内核只是根据头部信息创建对应的栈段描述符。所以我们要验证的就是栈段描述符和用户定义的栈是否相符。在上面提到的博文中,我已经详细推导了如何构造栈段描述符。
用户程序的头部结构如下图所示:
本实验的用户程序的符号表有3个符号,所以头部总长度为0x328
;由于用户程序的加载地址是0x100000
,所以栈空间的起始地址为0x100328
;又因为栈空间大小定义为0x1000(4KB)
,所以栈空间的高端物理地址为0x100328+0x1000=0x101328
,
这应该就是栈段描述符的基地址。
有效段界限应为0xFFFFFFFF-栈的大小(以字节为单位)
,即0xFFFFFFFF-0x1000=0xFFFFEFFF
,这应该是栈段描述符中的段界限。
3.3.2 验证结果
在Bochs中运行程序,跑起来后,Ctrl
+C
暂停程序,输入info gdt
可以查看GTD的信息。我们看到在下图中:
黄色划线的描述符与我们的推理相符。所以,我们的修改是正确的。
【end】