操作系统实验报告3:Linux 下 x86 汇编语言2

操作系统实验报告3

实验内容

  • 验证实验 Blum’s Book: Sample programs in Chapter 06, 07 (Controlling Flow and Using Numbers)

实验环境

  • 架构:Intel x86_64 (虚拟机)
  • 操作系统:Ubuntu 20.04
  • 汇编器:gas (GNU Assembler) in AT&T mode
  • 编译器:gcc

技术日志

Chapter 06

跳转指令

跳转指令使用单一指令码:

jmp location

其中location是要跳转到的内存地址

  • 验证实验jumptest.s

1.构建一般可执行程序:

程序的源代码略。

执行程序命令:

as --32 -o jumptest.o jumptest.s
ld -m elf_i386 -o jumptest jumptest.o
./jumptest
echo $?

执行截图:

分析:程序先把寄存器eax赋值为1,然后使用跳转指令跳过把寄存器ebx赋值为10,跳转到了把寄存器ebx赋值为20的语句,可以看到跳转确实发生了。

2.使用objdump程序进行反汇编:

执行程序命令:

as --32 -gstabs -o jumptest.o jumptest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o jumptest jumptest.o
objdump -D jumptest

执行截图:

分析:程序开始时使用的第一个内存位置是0x8049001,overhere标签指向的内存位置是0x8048083

3.使用gdb运行程序:

执行程序命令:

as --32 -gstabs -o jumptest.o jumptest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o jumptest jumptest.o
gdb -q jumptest

执行结果如下:

分析:

在程序的开始位置设置断点,并运行程序,查看使用的第一个内存位置,显示在寄存器eip中,这个值是0x8049001,它和objdump输出中显示的相同内存位置相对应,单步调试至执行了跳转指令,再次显示寄存器eip中的值,这个值是0x8048083,在objdump输出中显示,这是overhere标签指向的位置,说明实现跳转。

  • 验证实验calltest.s

执行程序命令:

as --32 -o calltest.o calltest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o calltest -lc calltest.o
./calltest

执行结果如下:

This is section 1
This is section 2
This is section 3

执行截图:

分析:在程序的开始,使用prinif显示第一个文本行,显示程序处于什么位位置。下一步, 使用call指令把控制转移到overhere标签。在overhere标签,寄存器esp的值被复制给指针ebp,以便在函数的结尾可以恢复它.再次使用prinf函数显示第二行文本,然后恢复esp和ebp寄存器。程序的控制返回到紧跟在call指令后面的指令,并且再次使用printf函数显示第三个文本行。

比较指令

CMP指令的格式如下:

cmp operand1, operand2

CMP指令把第二个操作数和第一个操作数进行比较。在幕后,它对两个操作数执行减法操作(operand2-operand1),比较指令不会修改这两个操作数,但是如果发生减法操作,就设置EFLAGS寄存器.

  • 验证实验cmptest.s

程序的源代码略。

执行程序命令:

as --32 -o cmptest.o cmptest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o cmptest cmptest.o
./cmptest
echo $?

执行结果如下:

分析:程序首先把15赋给寄存器eax,把10赋给寄存器ebx,再使用CMP指令比较这两个寄存器,按照比较的结果,使用JGB指令进行分支操作,因为寄存器ebx的值小于寄存器eax的值,所以不执行条件分支,转向下一条指令执行,将1存放到寄存器eax中,可以看到,寄存器ebx中的值确实仍是10,没有进行分支操作。

使用奇偶校检标志

奇偶校验标志表明数学运算答案中应该为1的位的数目。可以使用它作为粗略的错误检查系统.确保数学操作成功执行。

如果结果中被设况为1的位的数目是偶数,则设置奇偶校验位(置1)。如果设置为1的位的数目是奇数,则不设置奇偶校验位(置0)。

  • 验证实验paritytest.s

程序的源代码略。

执行程序命令:

as --32 -o paritytest.o paritytest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o paritytest paritytest.o
./paritytest
echo $?

执行结果如下:

分析:减法的结果为1,以二进制表示是00000001。因为为1的位的数目是奇数,所以不设置奇偶校检位,JP指令不会跳转到分支,程序退出,并且以减法的结果1作为结果代码。

为了测试相反的情况,把原程序中的:

subl $3, %ebx

改为:

subl $1, %ebx

分析:减法的结果是3,以二进制表示是00000011,因为为1的位的数目是偶数,所以设置奇偶校检位,并且JP指令应该转到overhere标签的分支,设置结果代码为100。

使用符号标志

符号标志使用在带符号数中,用于表示寄存器中包含的值的符号改变。在带符号数中,最后一位(最高位)用作符号位。它表明数字表示是负值(设置为1)还是正值(设置为0)。

  • 验证实验signtest.s

程序的源代码略。

执行程序命令:

as --32 -o signtest.o signtest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o signtest -lc signtest.o
./signtest

执行结果如下:

分析:signtest.s程序反向遍历数据数组,使用寄存器edi作为变址,处理每个数组元素时递减这个寄存器。使用JNS指令检杳寄存器edi的值什么时候变成负值,如果不是负值,则返回到循环的开头。

循环指令

循环指令基本格式:

loop address

其中address是要跳转到的程序代码位置的标签名称。循环开始前,必须在寄存器ecx中设置执行迭代的次数。

  • 验证实验loop.s

程序的源代码略。

执行程序命令:

as --32 -o loop.o loop.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o loop -lc loop.o
./loop

执行结果如下:

分析:循环指令执行100以及以内的正整数的相加指令,利用循环实现直到寄存器ecx的值为0,可以看到,结果为5050。

  • 验证实验betterloop.s

把loop.s原程序代码中的:

movl $100, %ecx

改为:

movl $0, %ecx

执行程序:

分析:将寄存器ecx设置为0时LOOP指令会将其递减为-1,然后继续执行下去,显示错误的值。所以需要使用JCXZ指令执行条件分支避免出错。

执行程序命令:

as --32 -o betterloop.o betterloop.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o betterloop -lc betterloop.o
./betterloop

执行结果如下:

分析:结果输出为0,确实正确的循环。

  • 验证实验ifthen.c

程序的源代码略。

执行程序命令:

gcc -m32 -S ifthen.c
cat ifthen.s

执行结果如下:

分析:实现if-then语句的汇编语言代码逻辑

  • 验证实验for.c

程序的源代码略。

执行程序命令:

gcc -m32 -S for.c
cat for.s

执行结果如下:

分析:实现for语句的汇编语言代码逻辑

Chapter 07

使用带符号整数
  • 验证实验inttest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o inttest.o inttest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o inttest inttest.o
gdb -q inttest

执行结果如下:

分析:调试器假设寄存器ebx和ecx包含带符号整数,并且使用我们期望的数据类型显示答案。但是寄存器edx出现了问题。因为调试器试图把整个寄存器edx作为带符号整数数据值显示,所以它假设整个寄存器edx包含一个双字带符号整数(32位)。因为寄存器edx只包含一个单字整数(16位),所以解释出的值是错误的。寄存器中的数据仍然是正确的(0xFFB1),但是调试器认为的这个数字表示的内容是错误的.

MOVZE指令

MOVZX指令把长度小的无符号整数值(可以在寄存器中,也可以在内存中)传送给长度大的无符号整数值(只能在寄存器中)。

MOVZX指令格式:

movzx source, destination

其中source可以是8位或16位寄存器或者内存位置,destination可以是16位或者32位寄存器。

  • 验证实验movzxtest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o movzxtest.o movzxtest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o movzxtest movzxtest.o
gdb -q movzxtest

执行结果如下:

分析:movzxtest.s程序简单地把一个大的值存放到寄存器ecx中,然后使用MOVZX指令把低8位 复制到寄存器ebx。因为存放在寄存器ecx中的值使用长度为字的无符号整数表示它(它大于255),所以CL中的值只表示完整值的一部分。

通过输出寄存器ebx和ecx的十进制值,马上就能发现无符号整数值没有被正确地复制,原始值为279,但是新的值只是23。通过按照十六进制显示值,可以发现为什么会这样。十六进制格式的原始值为0x0117,它占用一个双字。MOVZX指令只传送了寄存器ecx的低位字节,而用0填充了寄存器ebx中剩余的字节,这样就在寄存器ebx中生成了0x17这个值。

MOVSX指令

MOVSX指令允许扩展带符号整数并且保留符号,它假设要传送的字节是带符号整数格式,并且试图在传送过程中保持带符号整数的值不变。

  • 验证实验movsxtest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o movsxtest.o movsxtest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o movsxtest movsxtest.o
gdb -q movsxtest

执行结果如下:

分析:movsxtest.s程序在寄存器cx中(双字长度)定义一个负值。然后试图把这个值复制到寄存器ebx中,程序首先使用零填充寄存器ebx,然后使用MOV指令。下一步,使用MOVSX指令把寄存器cx的值传送给寄存器eax。

单步运行程序,一直运行到MOVSX指令之后,可以使用调试器的info命令显示寄存器值。寄存器ecx包含的值是0x0000FFB1,低16位包含的值是0xFFB1,它是带符号整数格式的-79。当寄存器cx被传送给寄存器ebx时,寄存器ebx包含的值是0x0000FFB1,它是带符号整数格式 的65457,这是不对的。

使用MOVSX指令把寄存器cx传送给寄存器eax之后,寄存器eax包含的值是0xFFFFFFB1,它是带符号整数格式的-79,MOVSX指令正确地为这个值添加了高位部分的1。

  • 验证实验movsxtest2.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o movsxtest2.o movsxtest2.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o movsxtest2 movsxtest2.o
gdb -q movsxtest2

执行结果如下:

分析:movsxtest2.s和movsxtest.s完成相同的工作,但是使用的是带符号整数正值。当寄存器cx被传送给空的寄存器ebx时。值的格式是正确的(因为高位部分的零对正数是没有问题的)。另外,MOVSX指令正确地使用零填充了寄存器eax,生成了正确的32位带符号整数值。

在GNU汇编器中定义整数

.quad命令可以定义一个或者多个带符号整数值

  • 验证实验quadtest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o quadtest.o quadtest.s
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o quadtest quadtest.o
gdb -q quadtest

执行结果如下:

分析:程序简单地在标签data1的位置定义一个包含5个双子带符号整数的数组,在标签data2的位置定义一个包含5个四字带符号整数的数组,然后退出程序,为了查看执行情况,再次对程序进行汇编并且在调试器中运行它。

首先,显示调试器认为的data1和data2数组的十进制值,data1数组的如期望,data2数组中的值不是程序中使用的值,这是因为调试器假设这些值是双字的带符号整数值。

然后查看内存中标签data1位置的数组值是如何存储的,可以看到,每个数组元素使用4个字节,并且按照小尾数格式存放。

接着查看存储在标签data2位置的数组值,可以看到,标签data2位置的数据值是使用四字编码的,所以每个值使用8个字节,汇编器把这些值放到了正确的位置,但是调试器不知道仅仅通过x/d命令如何显示这些值,需要使用gd选项显示这些值。

  • 验证实验mmxtest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o mmxtest.o mmxtest.s
ld -m elf_i386 -o mmxtest mmxtest.o
gdb -q mmxtest

执行结果如下:

分析:程序定义了两个数据数组。第一个数组(value1)定义2个双字带符号整数,第二个数组(value2)定义8个字节带符号整数值。使用MOVQ指令把这些值加载到前2个MMX寄存器中。

可以看到,单步运行到MOVQ指令之后,可以显示MM0和MM1寄存器中的值,显示寄存器时,调试器不知道寄存器中数据的格式是什么,所以它会显示所有可能的情况,第一个pprint命令把MM0寄存器的内容显示为双字整数值。因为前面的例子使用双字整数值,所以唯一有意义的显示格式是int32,它显示正确的信息。可以使用print/f命令使调试器只生成这一格式。

但是MM1寄存器包含字节整数值,所以不能按照十进制模式显示它。可以使用print命令的x参数显示寄存器中的原始字节,可以看到,各个字节被正确地存放到了MM1寄存器中。

传送SSE整数

MOVDQA和MOVDQU指令的简单格式:

movdqa source, destination

MOVDQA和MOVDQU指令用于把128位数据传送到XMM寄存器中,或者在XMM寄存器之间传送数据,对于对准16个字节边界的数据,就使用A选项,否则,就使用U选项,其中source和destination可以是SSE128位寄存器或者128位的内存地址。

  • 验证实验ssetest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o ssetest.o ssetest.s
ld -m elf_i386 -o ssetest ssetest.o
gdb -q ssetest

执行结果如下:

分析:程序定义了两个包含不同整数数据类型的数据数组。第一个数组(value1)定义4个双字带符号整数,第二个数组(value2)定义2个四字带符号整数值。使用MOVDQU指令把这两个数据数组传送到SSE寄存器中。

可以看到,MOVDQU指令执行之后,XMM0和XMM1寄存器包含数据段中定义的数据值。XMM0寄存器包含4个双字带符号整数数据值,XMM1寄存器包含2个四字带符号整数数据值。

传送BCD值

IA-32指令集包含处理80位打包BCD值的指令。可以使用FBLD和FBSTP指令把80位打包 BCD值加载到FPU寄存器中以及从FPU寄存器获取这些值。

使用FPU寄存器的方式和使用通用寄存器稍微有些区别。8个FPU寄存器的行为类似于内存中的堆栈区域。可以把值压入和弹出FPU寄存器池。ST0引用位于堆栈顶部的寄存器。当值被压
入FPU寄存器堆栈时.它被存放在ST0寄存器中,ST0中原来的值被加载到ST1中。

  • 验证实验bcdtest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o bcdtest.o bcdtest.s
ld -m elf_i386 -o bcdtest bcdtest.o
gdb -q bcdtest

执行结果如下:

分析:bcdtest.s程序在标签data1定义的内存位置创建一个表示十进制值1234的简单的BCD值(记住Intel使用小尾数表示法)。使用FBLD指令把这个值加载到FPU寄存器堆栈的顶部( ST0)。使用FIMUL指令把ST0寄存器和data2所在的内存位置中的整数值相乘。最后,使用FBSTP指令把堆栈中新的值传送回data1所在的内存位置中。

首先,在执行程序前,使用x/10xb &data1查看data1所在的内存位置值。1234的BCD值被加载到了data1所在的内存位置。

然后,单步执行FBLD指令,并且使用info all命令查看ST0寄存器中的值,ST0寄存器的值应该显示它加载了十进制值1234,但是这个寄存器的十六进制不是80位打包BCD格式,在FPU中,BCD值被转换成了浮点表示方式。

接着单步执行下一条指令(FIMUL),并且再次查看寄存器,发现ST0寄存器中的值和2相乘了。

最后把ST0中的值存放回data1所在的内存位置中,使用x/10xb &data1显示这个内存位置,可以看到,新的值被存放到了data1所在的内存位置,并且转换回了BCD格式。

传送浮点值

FLD指令用于把浮点值传送入和传送出FPU寄存器。FLD指令的格式是:

fld source

其中source可以是32位、64位或者80位内存位置。

  • 验证实验floattest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o floattest.o floattest.s
ld -m elf_i386 -o floattest floattest.o
gdb -q floattest

执行结果如下:

分析:标签value1指向存储在4个字节内存中的单精度浮点值。标签value2指向存储在8个字节内存 中的双精度浮点值。标签data指向内存中的空缓冲区,它将被用于传输双精度浮点值。

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

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

首先使用x/f &value1x/gf &value2查看十进制值。f选项显示单精度数字,需要使用gf选项显示双精度值,显示四字值,当调试器试图计算要显示的值时,已经存在舍入错误。

接着,使用x/4xb &value1x/8xb &value2查看浮点值是如何存储在内存位置的。

然后,单步运行第一条FLDS指令,使用print $st0查看ST0寄存器的值,可以看到,位于value1内存位置中的值12.340000152587890625被正确存放到了ST0寄存器中。

接下来,单步运行下一条指令,并且查看ST0寄存器中的值,这个值已经被替换为新加载的双精度值2353.63099999999985812

为了查看对原来加载的值进行了什么处理,查看ST1寄存器,发现当加载新的值时,ST0中的值被下移到了ST1寄存器中。

再查看data标签的值,单步执行FSTL指令,并且再次查看,发现FSTL指令把ST0寄存器中的值加载到了data标签指向的内存位置。

  • 验证实验fpuvals.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o fpuvals.o fpuvals.s
ld -m elf_i386 -o fpuvals fpuvals.o
gdb -q fpuvals

执行结果如下:

分析:程序简单地把各个浮点常量压入到FPU寄存器堆栈中。值的顺序和它们被存放到堆栈中的顺序是相反的。

  • 验证实验ssefloat.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o ssefloat.o ssefloat.s
ld -m elf_i386 -o ssefloat ssefloat.o
gdb -q ssefloat

执行结果如下:

分析:程序创建两个数据数组,每个数组由4个单精度浮点值组成。它们将成为被存储到XMM寄存器中的打包数据值,还创建了一个数据缓冲区。它有足够的空间保存4个单精度浮点值(即一个打包的值)。然后程序使用MOVUPS指令在XMM寄存器和内存之间传送打包单精度浮点值。

可以看到,所以数据都被正确地加载到了XMM寄存器中。v4_float格式显示使用的打包单精度浮点值。最后是把XMM寄存器的值复制到data位置。可以使用x/4f命令显示结果。

按照十六进制复查答案,发现内存位置data和内存位置value1中的值是匹配的。

  • 验证实验sse2float.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o sse2float.o sse2float.s
ld -m elf_i386 -o sse2float sse2float.o
gdb -q sse2float

执行结果如下:

分析:存储在内存中的数值被改为双精度浮点值。因为程序将传输打包值,所以创建了一个包含2个值的数组。

可以看到,对于v2_double数据类型,正确的值已经被传送到了寄存器中。因为内存位置data包含2个双精度浮点值,所以使用x/2gf命令显示存储在这个内存位置的2个值,发现确实正确的值也被复制到了这里。

转换指令
  • 验证实验convtest.s

程序的源代码略。

执行程序命令:

as --32 -gstabs -o convtest.o convtest.s
ld -m elf_i386 -o convtest convtest.o
gdb -q convtest

执行结果如下:

分析:convtest.s程序在内存位置value1定义一个打包单精度浮点值,在内存位置value2定义一个打包双字整数值。第一对指令可以比较CVTPS2DQ和CVTTPS2DQ指令的结果。第一条指令执行一般的舍入,第二条指令通过向零方向舍入进行截断。

按照v4_int32格式,值被正确地显示出来,正如所见,一般转换把浮点值124.79舍入为125。但是截断转换把它向零方向舍入,使之成为124。内存位置data的值被转换为打包双字整数后,可以使用x/4d命令显示它。可以看到,显示出了舍入后的整数值。

遇到问题

1.当运行signtest.s时,会发生报错,显示Error: can't open signtest.s for reading: No such file or directory

原因是课本提供的原来的代码的有一行的的寄存器出错:

add $8, $esp

解决方案:这行代码应该改为:

add $8, %esp

执行截图:

2.有时候按照课本的方式进行gdb调试,比如运行quadtest.s程序,使用x/20b &data1想以十六进制显示data1数组里的数值时,最后显示的是十进制的数值。

解决方法:把x/20b &data1改为x/20xb &data1,在代表显示数值长度的n=20后面加上x,就可以以十六进制显示数字,其它的程序显示时也是一样。

执行截图:

3.当运行convtest.s时,会发生报错,显示Error: symbol `data' is already defined

原因是课本提供的原来的代码的data命名重复了

data:
    .lcomm data, 16

movdqu %xmm0, data

都与开头的.section .data重复

解决方案:代码应该改为:

data1:
    .lcomm data, 16

movdqu %xmm0, data1

执行截图:

4.当运行ifthen.c和for.c时,如果按照课本上给的指令编译:

gcc -S ifthen.c
cat ifthen.s

gcc -S for.c
cat for.s

生成的.s文件是64位的,与课本上的代码不符。

解决方案:

应该使用32位命令进行编译:

gcc -m32 -S ifthen.c
cat ifthen.s

gcc -m32 -S for.c
cat for.s

这样生成的.s文件就是32位的,与课本上的代码相符。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值