操作系统实验报告4:Linux 下 x86 汇编语言3
操作系統(tǒng)實驗報告4
實驗內(nèi)容
- 驗證實驗 Blum’s Book: Sample programs in Chapter 08, 10 (Basic Math Functions and Using Strings)
實驗環(huán)境
- 架構(gòu):Intel x86_64 (虛擬機(jī))
- 操作系統(tǒng):Ubuntu 20.04
- 匯編器:gas (GNU Assembler) in AT&T mode
- 編譯器:gcc
技術(shù)日志
Chapter 08
加法指令
ADD指令用于把兩個整數(shù)相加,指令格式如下:
add source, destination其中source可以是立即值、內(nèi)存位置或者寄存器。destination參數(shù)可以是寄存器或者內(nèi)存位置中存儲的值(但是不能同時使用內(nèi)存位置作為源和目標(biāo))。加法的結(jié)果存放在目標(biāo)位置。
ADD指令可以將8位、16位或者32位值相加。和其他GNU匯編器指令一樣,必須通過在ADD助記符的結(jié)尾添加b(用于字節(jié))、w(用于字)或者l(用于雙字)來指定操作數(shù)的長度。
- 驗證實驗addtest1.s
在程序的源代碼的最后:
movl $1, %eax這一行前,加上:
end:movl $1, %eax便于進(jìn)行斷點調(diào)試。
執(zhí)行程序命令:
as --32 -gstabs -o addtest1.o addtest1.s ld -m elf_i386 -o addtest1 addtest1.o gdb -q addtest1執(zhí)行截圖:
分析:和課本預(yù)期的輸出結(jié)果一致,對無符號數(shù)的加法執(zhí)行正確。
- 驗證實驗addtest2.s
在程序的源代碼的最后:
movl $1, %eax這一行前,加上:
end:movl $1, %eax便于進(jìn)行斷點調(diào)試。
執(zhí)行程序命令:
as --32 -gstabs -o addtest2.o addtest2.s ld -m elf_i386 -o addtest2 addtest2.o gdb -q addtest2執(zhí)行截圖:
分析:和課本預(yù)期的輸出結(jié)果一致,對帶符號整數(shù)的加法執(zhí)行也正確。
驗證實驗addtest3.s
執(zhí)行程序命令:
as --32 -gstabs -o addtest3.o addtest3.s ld -m elf_i386 -o addtest3 addtest3.o ./addtest3 echo $?執(zhí)行結(jié)果如下:
改動寄存器的值,使加法不產(chǎn)生進(jìn)位,把原程序代碼中的:
movb $190, %bl movb $100, %al改為:
movb $190, %bl movb $10, %al執(zhí)行結(jié)果如下:
分析:程序?qū)Υ鎯υ贏L和BL寄存器中的2字節(jié)無符號整數(shù)值執(zhí)行簡單的加法。如果加法操作造成進(jìn)位,則把進(jìn)位標(biāo)志設(shè)置為1,并且JC指令將跳轉(zhuǎn)到標(biāo)簽over。程序的結(jié)果代碼要么是加法的結(jié)果,要么就是0值(如果結(jié)果超過255)。
第一個程序設(shè)置寄存器值使加法產(chǎn)生進(jìn)位,運(yùn)行程序,然后使用echo命令查看結(jié)果代碼,結(jié)果代碼為0,表示正確檢測到了進(jìn)位情況。
第二個程序改動寄存器的值,使加法不產(chǎn)生進(jìn)位,運(yùn)行程序之后,加法沒有產(chǎn)生進(jìn)位,沒有跳轉(zhuǎn),并且加法的結(jié)果被設(shè)置為結(jié)果代碼200。
- 驗證實驗addtest4.s
執(zhí)行程序命令:
as --32 -o addtest4.o addtest4.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o addtest4 -lc addtest4.o ./addtest4執(zhí)行截圖:
分析:程序試圖把兩個大的負(fù)數(shù)相加,這造成了溢出情況。JO指令用于檢查溢出并且把控制傳遞到標(biāo)簽over。運(yùn)行程序,輸出0,這表明檢測到了溢出情況。
把原程序代碼中的:
movl $-1590876934, %ebx movl $-1259230143, %eax改為:
movl $-190876934, %ebx movl $-159230143, %eax執(zhí)行截圖:
分析:修改MOVL指令,使兩個值相加不產(chǎn)生溢出情況,就會看到加法的結(jié)果。
ADC指令
使用ADC指令處理非常大的、不能存放到雙字?jǐn)?shù)據(jù)長度中的帶符號或者無符號整數(shù)的相加。
ADC指令的格式如下:
adc source, destination其中source可以是立即值或者8位、16位或者32位寄存器或內(nèi)存位置值,destination可以是8位、16位或者32位寄存器或內(nèi)存位置值。
- 驗證實驗adctest.s
在原程序代碼中的:
addl %ebx, %edx adcl %eax, %ecx pushl %ecx pushl %edx加上:
allmov:addl %ebx, %edxadcl %eax, %ecx alladd:pushl %ecxpushl %edx便于加斷點調(diào)試
執(zhí)行程序命令:
as --32 -gstabs -o adctest.o adctest.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o adctest -lc adctest.o gdb -q adctestas --32 -gstabs -o adctest.o adctest.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o adctest -lc adctest.o ./adctest執(zhí)行結(jié)果如下:
分析:程序把兩個64位值相加,一個值保持在EAX:EBX寄存器組合中,另一個值保持在ECX:EDX寄存器組合中。
可以看到,執(zhí)行加法操作之后,64位整數(shù)的十六進(jìn)制值被加載到了寄存器中,ECX:EDX寄存器對包含結(jié)果數(shù)據(jù),使用printf函數(shù)也顯示出了十進(jìn)制形式的結(jié)果。
減法指令
SUB指令的格式如下:
sub source, destination其中從destination的值中減去source的值,結(jié)果存儲在destination操作數(shù)的位置。source可以是立即值或者8位、16位或者32位寄存器或內(nèi)存位置值,destination可以是8位、16位或者32位寄存器或內(nèi)存位置值。
- 驗證實驗subtest1.s
在原程序代碼中的:
subl %eax, data
movl $1, %eax
加上:
end:subl %eax, datamovl $1, %eax便于加斷點調(diào)試
執(zhí)行程序命令:
as --32 -gstabs -o subtest1.o subtest1.s ld -m elf_i386 -o subtest1 subtest1.o gdb -q subtest1執(zhí)行結(jié)果如下:
分析:內(nèi)存位置data1的值(40)減去EAX寄存器中的值(-30),得到正確的結(jié)果70。
減法操作中的進(jìn)位和溢出
- 驗證實驗subtest2.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o subtest2.o subtest2.s ld -m elf_i386 -o subtest2 subtest2.o ./subtest2 echo $?執(zhí)行結(jié)果如下:
分析:對于無符號整數(shù),從2中減去5,可以看到,當(dāng)結(jié)果小于0時,進(jìn)位標(biāo)志被設(shè)置為1,發(fā)生跳轉(zhuǎn),程序的結(jié)果代碼為0,檢查EBX寄存器中的值,發(fā)現(xiàn)為-3,盡管它被“認(rèn)為是”無符號整數(shù),但是由程序負(fù)責(zé)確定值是否超出了無符號(或者帶符號)值的范圍,只能使用進(jìn)位標(biāo)志確定無符號整數(shù)的減法產(chǎn)生負(fù)數(shù)結(jié)果的情況,如果執(zhí)行帶符號整數(shù)的減法,進(jìn)位標(biāo)志是沒有用處的,因為結(jié)果長常常可能是負(fù)值,所以要依靠溢出標(biāo)志來判斷到達(dá)了數(shù)據(jù)長度界限的情況。
依靠溢出標(biāo)志來判斷到達(dá)了數(shù)據(jù)長度界限的情況
- 驗證實驗subtest3.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -o subtest3.o subtest3.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o subtest3 -lc subtest3.o ./subtest3執(zhí)行結(jié)果如下:
分析:程序演示從存儲在EBX寄存器中的負(fù)值中減去存儲在EAX寄存器中的正值,生產(chǎn)一個超過32位EBX寄存器范圍的值。JO指令用于檢測溢出標(biāo)志,并且把程序轉(zhuǎn)到over,把輸出設(shè)置位0。
可以看到,溢出情況被檢測到,并且執(zhí)行JO指令和進(jìn)行跳轉(zhuǎn)。
為了測試程序在相反的情況是否正常工作,可以把EAX寄存器的值改為負(fù)值,把原程序中的:
movl $1259230143, %eax改為:
movl $-1259230143, %eax分析:程序減去負(fù)數(shù)生成一個絕對值更小的負(fù)數(shù),它在數(shù)據(jù)長度的界限之內(nèi),沒有設(shè)置溢出標(biāo)志。
SBB指令
可以使用進(jìn)位情況幫助執(zhí)行大的無符號整數(shù)值的減法操作,SBB指令在多字節(jié)減法操作中利用進(jìn)位和溢出標(biāo)志實現(xiàn)跨越數(shù)據(jù)邊界的借位特性。
SBB指令的格式如下:
sbb source, destination其中進(jìn)位位被添加到source值,從destination的值中減去source的值,結(jié)果存儲在destination操作數(shù)的位置。source可以是8位、16位或者32位寄存器或內(nèi)存位置值,destination可以是8位、16位或者32位寄存器或內(nèi)存位置值,不能同時使用內(nèi)存位置作為源和目標(biāo)值。
- 驗證實驗sbbtest.s
執(zhí)行程序命令:
as --32 -o sbbtest.o sbbtest.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o sbbtest -lc sbbtest.o ./sbbtest執(zhí)行結(jié)果如下:
分析:結(jié)果與課本上預(yù)期的結(jié)果一樣,得到了正確的減法值。
乘法指令
MUL指令用于兩個無符號整數(shù)相乘,MUL指令的格式如下:
mul source其中source可以是8位、16位或者32位寄存器或內(nèi)存值。
無符號整數(shù)乘法需求:
| 8位 | AL | AX |
| 16位 | AX | DX:AX |
| 32位 | EAX | EDX:AX |
- 驗證實驗multest.s
在原程序代碼中的:
pushl %edx pushl %eax加上:
aftermul:pushl %edxpushl %eax便于加斷點調(diào)試
執(zhí)行程序命令:
as --32 -gstabs -o multest.o multest.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o multest -lc multest.o gdb -q multest ./multest執(zhí)行結(jié)果如下:
分析:程序演示兩個32位無符號整數(shù)的乘法操作,并且從EDX:EAX寄存器獲得結(jié)果。
寄存器對組合EDX:EAX生產(chǎn)結(jié)果值,這個值被存儲在內(nèi)存位置result中,并且通過printf函數(shù)顯示出來,與課本上的預(yù)期的結(jié)果一致。
帶符號整數(shù)乘法
MUL指令只能用于無符號整數(shù),而IMUL指令可以用于帶符號和無符號整數(shù)、但是必須小心結(jié)果不使用目標(biāo)的最高有效位,對于較大的值,IMUL指令只對帶符號整數(shù)是合法的。為了應(yīng)付比較復(fù)雜的情況,IMUL指令而3種不同的指令格式:
IMUL指令的第一種格式使用一個操作數(shù),其行為和MUL指令完全一樣:
imul sourcesource操作數(shù)可以是8位、16位或者32位寄存器或內(nèi)存中的值,它與位于AL、AX或者EAX寄存器(取決于源操作數(shù)的長度)中的隱含操作數(shù)相乘。然后,結(jié)果被存放到AX寄存器、DX:AX寄存器對或者EDX:EAX寄存器對中。
IMUL指令的第二種格式允許指定EAX寄存器之外的目標(biāo)操作數(shù):
imul source, destination其中source可以是16位或者32位寄存器或內(nèi)存中的值,destination必須是16位或者32位通用寄存器。這種格式允許指定把乘法操作的結(jié)果存放到哪個位置(而不是強(qiáng)制使用AX和DX寄存器)。
這種格式的缺陷在于乘法操作的結(jié)果被限制為單一目標(biāo)寄存器的長度(非64位結(jié)果)。使用這種格式時必須非常小心,不要溢出目標(biāo)寄存器。
IMUL指令的第三種格式允許指定3個操作數(shù):
imul multiplier, source destination其中multiplier是一個立即值,source是16位或者32位寄存器或內(nèi)存中的值,destination必須是通用寄存器。這種格式允許執(zhí)行一個值(source)和一個帶符號整數(shù)(multiplier)的快速乘法操作,把結(jié)果存儲到通用寄存器(destination)中.
- 驗證實驗imultest.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o imultest.o imultest.s ld -m elf_i386 -o imultest imultest.o gdb -q imultest執(zhí)行結(jié)果如下:
分析:EAX寄存器包含EDX寄存器的值(400)和立即值2相乘得到的結(jié)果。ECX寄存器包含EBX寄存器的值(10)和最初加載到ECX寄存器中的值(-35)相乘的結(jié)果。注意,結(jié)果作為帶符號整數(shù)值被存放到ECX寄存器中。
帶符號整數(shù)乘法檢查溢出
當(dāng)使用帶符號整數(shù)和IMUL指令時,總是要檢查結(jié)果中的溢出。一種方式是使用JO指令檢查溢出標(biāo)志。
- 驗證實驗imultest2.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o imultest2.o imultest2.s ld -m elf_i386 -o imultest2 imultest2.o gdb -q imultest2執(zhí)行結(jié)果如下:
分析:程序杷兩個值傳送到16位寄存器中(AX和CX),然后使用16位IMUL指令將它們相乘。設(shè)置結(jié)果會導(dǎo)致16位寄存器溢出,并且JO指令跳轉(zhuǎn)到標(biāo)簽over,這里退出程序,帶有結(jié)果代碼1。
修改加載到寄存器中的立即數(shù)值,使結(jié)果小于65535,把原程序中的:
movw $680, %ax改為:
movw $60, %ax執(zhí)行結(jié)果如下:
分析:如果修改加載到寄存器中的立即數(shù)值,使結(jié)果小于65535, IMUL指令就不會把溢出標(biāo)志設(shè)置為1,不會執(zhí)行JO指令,程序退出,帶有結(jié)果代碼0。
除法指令
DIV指令用于無符號整數(shù)的出發(fā)操作。DIV指令的格式如下:
div divisor其中divisor(除數(shù))是隱含的被除數(shù)要除以的值,它可以是8位、16位或者32位寄存器或內(nèi)存中的值。在執(zhí)行DIV指令之前,被除數(shù)必須已經(jīng)存儲到了AX寄存器(對于16位值)、DX:AX寄存器對(對于32位值)或者EDX:EAX寄存器對(對于64位值)。
允許的除數(shù)的最大值取決于被除數(shù)的長度。對于16位被除數(shù),除數(shù)只能是8位;對于32位被除數(shù),除數(shù)只能是16位;對于64位被除數(shù),除數(shù)只能是32位。
除法操作的結(jié)果是兩個單獨的數(shù)字:商和余數(shù)。這兩個值都存儲在被除數(shù)值使用的相同寄存器中。下表列出了其設(shè)置的情況。
| AX | 16位 | AL | AH |
| DX:AX | 32位 | AX | DX |
| EDX:EAX | 64位 | EAX | EDX |
這就是說,當(dāng)除法操作完成時,會丟失被除數(shù),所以要確保這不是這個值的唯一拷貝(除非在除法操作之后就不需要被除數(shù)的值了)。還要記住,結(jié)果會改變DX或者EDX寄存器的值,所以也要小心其中存儲的內(nèi)容。
- 驗證實驗divtest.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o divtest.o divtest.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o divtest -lc divtest.o ./divtest執(zhí)行結(jié)果如下:
分析:程序把一個64位四字按數(shù)加栽到EDX:EAX寄存器對中,然后使用一個存儲在內(nèi)存中的32位雙字整數(shù)除這個值。32位的商值存儲在一個內(nèi)存位置中,32位的余數(shù)值存儲在另一個內(nèi)存位置中。
修改除數(shù)的值為0,檢測除以0的情況,把原程序中的:
divisor:.int 25改為:
divisor:.int 0執(zhí)行結(jié)果如下:
分析:發(fā)生除以0的情況時會產(chǎn)生錯誤,系統(tǒng)會產(chǎn)生中斷,需要進(jìn)行檢查。
移位乘法
為了使整數(shù)乘以2的乘方,必須把值向左移位。可以使用兩個指令使整數(shù)值向左移位,SAL(向左算術(shù)移位)和SHL(向左邏輯移位)。這兩個指令執(zhí)行相同的操作,并且是可以互換的。它們有3種不同格式:
sal destination sal %c1, destination sal shifter, destination第一種格式把destination的值向左移1位,這等同于使值乘以2。
第二種格式把destination的值向左移動CL寄存器中指定的位數(shù)。
最后一個版本把destination的值向左移動shifter值指定的位數(shù)。在所有的格式中,目標(biāo)操作數(shù)可以是8位、16位或者32位寄存器或內(nèi)存中的值。
- 驗證實驗saltest.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o saltest.o saltest.s ld -m elf_i386 -o saltest saltest.o gdb -q saltest執(zhí)行結(jié)果如下:
分析:一開始,十進(jìn)制值10被加載到EBX寄存器中。第一條SAL指令把它移動1位(使之乘以2,結(jié)果為20)。第二條SAL指令把它移動2位(使之乘以4,結(jié)果為80),第三條SAL指令把它再移動2位(使之乘以4,結(jié)果為320)。value1位置中的值(25)被移動1位(使之為50),然后再移動2位(使之為200)。
把二進(jìn)制結(jié)果轉(zhuǎn)化為不打包BCD格式的指令(AAA為調(diào)整加法操作的結(jié)果)
- 驗證實驗aaatest.s
在原程序代碼中的:
movl $1, %eax movl $0, %ebx加上:
end:movl $1, %eaxmovl $0, %ebx便于加斷點調(diào)試
執(zhí)行程序命令:
as --32 -gstabs -o aaatest.o aaatest.s ld -m elf_i386 -o aaatest aaatest.o gdb -q aaatest執(zhí)行結(jié)果如下:
分析:第三次執(zhí)行ADC指令后,AL寄存器中包含的值為10,顯示9和1的二進(jìn)制加法結(jié)果為10。
執(zhí)行AAA指令后,AX寄存器的值為0x100 256,它顯示AH寄存器中的不打包值為1,AL寄存器中的值為0。1被帶人到下一位的值的加法操作。
最后,結(jié)果按照不打包BCD格式存放到內(nèi)存位置sum中。
調(diào)整SUB或者SBB指令
- 驗證實驗dastest.s
執(zhí)行程序命令:
as --32 -gstabs -o dastest.o dastest.s ld -m elf_i386 -o dastest dastest.o gdb -q dastest執(zhí)行結(jié)果如下:
分析:程序把第一個打包BCD值加載到AL寄存器中(每次一個十進(jìn)制位)。然后使用SBB指令從它減去第二個打包BCD值。這樣,前一次減法操作留下的任何進(jìn)位位都會被考慮在內(nèi)。然后使用DAS指令把結(jié)果轉(zhuǎn)換為將存儲在內(nèi)存位置result中的打包BCD格式。ECX寄存器用于控制必須經(jīng)過的循環(huán)次數(shù)(每個打包BCD字節(jié)一次)。轉(zhuǎn)換之后,如果留有剩下的進(jìn)位位,就把它存放在結(jié)果值中。
第一個減法操作之后,EAX寄存器的值為0x0e 14
執(zhí)行DAS指令之后,這個值改變?yōu)?x08 8
它表示結(jié)果的第一個十進(jìn)制位。
- 驗證實驗cpuidtest.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o cpuidtest.o cpuidtest.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o cpuidtest -lc cpuidtest.o ./cpuidtest執(zhí)行結(jié)果如下:
分析:程序首先使用PUSHFL指令把EFLAGS寄存器的值保存到堆棧頂部。然后,使用POPL指令把EFLAGS值讀取到EAX寄存器中。
下一個步驟演示如何使用XOR指令設(shè)置寄存器的一位。使用MOVL指令把EFLAGS值的拷貝
保存到EDX寄存器中,然后使用XOR指令設(shè)置ID位(仍然在EAX寄存器中)為值1。XOR指令使用一個設(shè)置了ID位的立即值。EAX寄存器經(jīng)過異或操作之后,就確保ID位被設(shè)置為1了。下一個步驟把新的EAX寄存器值壓入到堆棧中,然后使用POPFL指令把它存儲在EFLAGS寄存器中。
現(xiàn)在必須確定是否成功地設(shè)置了ID標(biāo)志。再一次使用PUSHFL指令把EFLAGS寄存器壓入堆棧,然后使用POPL指令把它彈出到EAX寄存器中。這個值和原始的EFLAGS值(先前存儲在EDX寄存器中)進(jìn)行XOR操作,查看值改變成了什么。
最后,使用TEST指令查看ID標(biāo)志位是否改變了。如果是,那么EAX中的值就不為零,然后使用JNZ指令進(jìn)行跳轉(zhuǎn),輸出適當(dāng)?shù)南ⅰ?/p>
Chapter 10
傳送字符串
創(chuàng)建MOVS指令是為了把字符串從一個內(nèi)存位置傳送到另一個內(nèi)存位置,MOVS指令有3種格式:
- MOVSB:傳送單一字節(jié)
- MOVSW:傳送一個字(2字節(jié))
- MOVSL:傳送一個雙字(4字節(jié))
MOVS指令使用隱含的源和目標(biāo)操作數(shù)。隱含的源操作數(shù)是ESI寄存器。它指向源字字符串的內(nèi)存位置。隱含的目標(biāo)操作數(shù)是EDI寄存器。它指向字符串要被復(fù)制到的目標(biāo)內(nèi)存位置。記住操作數(shù)順序的好方法是ESI中的S代表源(source),而EDI中的D代表目標(biāo)(destination)。
使用GNU匯編器時,有兩種方式加載ESI和EDI值。第一種方式是使用間接尋址。通過在內(nèi)存位置標(biāo)簽前面添加$,內(nèi)存位置的地址被加載到了ESI或者EDI寄存器中:
movl $output, %edi這條指令把output標(biāo)簽的32位內(nèi)存位置傳送給EDI寄存器。
指定內(nèi)存位置的另一種方式是LEA指令。LEA指令加載一個對象的有效地址。因為Linux使用32位值引用內(nèi)存位置,所以對象的內(nèi)存地址必須存儲在32位的目標(biāo)值中,源操作數(shù)必須指向一個內(nèi)存位置,比如.data段中使用的標(biāo)簽。指令
leal output, %edi把output標(biāo)簽的32位內(nèi)存位置加載到EDI寄存器中。
- 驗證實驗movstest1.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o movstest1.o movstest1.s ld -m elf_i386 -o movstest1 movstest1.o gdb -q movstest1執(zhí)行結(jié)果如下:
分析:程序把內(nèi)存位置value1的位置加載到ESI寄存器中。把output內(nèi)存位置的位置加載到EDI寄存器中。當(dāng)執(zhí)行MOVSB指令時,它把1字節(jié)的數(shù)據(jù)從value1位置傳送到output位置。因為在.bss段中聲明output變量,所以存放在這里的任何字符串?dāng)?shù)據(jù)的結(jié)尾會被自動地加上空字符。
可以看到,MOVSB指令把“T”從value1位置傳送到output位置。但是,無須改變ESI和EDI寄存器,當(dāng)運(yùn)行MOVSW指令時.它沒有傳送"Th"(字符串的前2個字節(jié)),而是把"hi"從value1位置傳送到了output位置。然后MOVSL指令繼續(xù)添加下4個字節(jié)的值。
- 驗證實驗movstest2.s
使用STD指令時,ESI和EDI寄存器在每條MOVS指令執(zhí)行之后遞減,所以它們應(yīng)該指向字
符串的末尾,而不是開頭。
在原程序代碼中的:
movl $1, %eax movl $0, %ebx加上:
end:movl $1, %eaxmovl $0, %ebx便于加斷點調(diào)試
執(zhí)行程序命令:
as --32 -gstabs -o movstest2.o movstest2.s ld -m elf_i386 -o movstest2 movstest2.o gdb -q movstest2執(zhí)行結(jié)果如下:
分析:這一次,value1內(nèi)存位置的地址位置被存放到EAX寄存器中,測試字符串的長度(減去1是因為字符串從地址0開始)與它相加。這個值被存放到ESI寄存器中。這使ESI寄存器指向測試字符串的末尾。對EDI進(jìn)行相同的操作,使它指向內(nèi)存位置output的末尾,STD指令用于設(shè)置DF標(biāo)志,使ESI和EDI寄存器在每條MOVS指令執(zhí)行之后遞減。
3條MOVS指令在兩個字符串位置之間傳送1、2和4個字節(jié)的數(shù)據(jù),output位置的字符串從字符串的末尾開始填充,但是3條MOVS指令執(zhí)行之后,只有4個內(nèi)存位置被填充了。在向前移動的movstest1.s程序中,使用相同的3條指令卻填充了7個內(nèi)存位置。這是因為盡管ESI和EDI寄存器向后計數(shù)。MOVSW和MOVSL指令還是按照向前的順序獲得內(nèi)存位置。當(dāng)MOVSB指令完成時,它使ESI和EDI寄存器遞減1,但是M0VSW指令獲得兩個內(nèi)存位置。同樣,當(dāng)M0VSW指令完成時,它使ESI和EDI寄存器遞減2,但是MOVSL指令獲得4個內(nèi)存位置。
- 驗證實驗movstest3.s
把MOVSL指令放在循環(huán)中,通過把ECX寄存器設(shè)置為字符串的長度來進(jìn)行控制。
在原程序代碼中的:
movl $1, %eax movl $0, %ebx加上:
end:movl $1, %eaxmovl $0, %ebx便于加斷點調(diào)試
執(zhí)行程序命令:
as --32 -gstabs -o movstest3.o movstest3.s ld -m elf_i386 -o movstest3 movstest3.o gdb -q movstest3執(zhí)行結(jié)果如下:
分析:ESI和EDI寄存器被設(shè)置為源和目標(biāo)內(nèi)存位置。ECX寄存器被設(shè)置為要傳送的字符串的長度。循環(huán)部分持續(xù)地執(zhí)行MOVSB指令。直到整個字符串傳送完畢。可以看到,查看內(nèi)存位置output的字符串值與課本上的預(yù)期輸出一樣。
REP前綴
REP指令用于按照特定次數(shù)重復(fù)執(zhí)行字符串指令,由ECX寄存器中的值進(jìn)行控制。這和使用循環(huán)類似,但是不需要額外的LOOP指令。REP指令重復(fù)地執(zhí)行緊跟在它后面的字符串指令,直到ECX寄存器中的值為零。
- 驗證實驗reptest1.s
MOVSB指令可以和REP指令一起使用,每次1字節(jié)地把字符串傳送到另一個位置。
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o reptest1.o reptest1.s ld -m elf_i386 -o reptest1 reptest1.o gdb -q reptest1執(zhí)行結(jié)果如下:
分析:要傳送的字符串長度被加載到ECX寄存器中,然后使用REP指令執(zhí)行MOVSB指令23次(字符串的長度),每次傳送1字節(jié)的數(shù)據(jù)。在調(diào)試器中單步執(zhí)行程序時,REP指令仍然只被算作一個指令步驟,而不是23個。
雖然單步執(zhí)行指令時,REP指令只占用一個步驟,但是在這個步驟之后,源字符申的所有23個字節(jié)都被傳送到了目標(biāo)字符串位置。
- 驗證實驗reptest2.s
也可以使用MOVSW和MOVSL指令在每次迭代中傳送1字節(jié)以上的數(shù)據(jù)。
如果使用MOVSW和MOVSL指令,ECX寄存器就應(yīng)該包含遍歷字符串所需的迭代次數(shù)。例如,如果要傳送8字節(jié)的字符串,如果使用MOVSB指令的話,就需要把ECX設(shè)置為8,使用MOVSW指令就設(shè)置為4,使用MOVSL指令就設(shè)置為2。
使用MOVSW和MOVSL指令遍歷字符串時,小心不要超出字符串的邊界。
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o reptest2.o reptest2.s ld -m elf_i386 -o reptest2 reptest2.o gdb -q reptest2執(zhí)行結(jié)果如下:
分析:程序通過6次循環(huán),傳送每個5字節(jié)的數(shù)據(jù)塊。但是源字符串的整個數(shù)據(jù)長度并不正好是4的倍數(shù)。最后一次執(zhí)行MOVSL指令時,它不僅獲得value1字符串的末尾,而且會錯誤地獲得定義的下一個字符串一字節(jié)的數(shù)據(jù)。
可以看到,output字符串的輸出包含value2字符串的第一個字符,它被添加到了value1字符串中,是錯誤的結(jié)果。
- 驗證實驗reptest3.s
當(dāng)知道字符串的長度時,就容易執(zhí)行整數(shù)除法以便確定字符串中包含多少個雙字。然后余數(shù)可以使用MOVSB指令進(jìn)行傳送(迭代次數(shù)應(yīng)該小于3次)。
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o reptest3.o reptest3.s ld -m elf_i386 -o reptest3 reptest3.o gdb -q reptest3執(zhí)行結(jié)果如下:
分析:程序把源和目標(biāo)內(nèi)存位置加載到ESI和EDI寄存器中,但是然后把字符串長度值加載到AX寄存器中。為了使字符串長度被4整除,使用SHR指令把長度值向右移動2位(這和被4整除相同),再把商值加載到ECX寄存器中。然后使REP MOVSL指令組合執(zhí)行這個值指定的次數(shù)。完成之后,確定余數(shù)值。
如果除數(shù)是2的乘方,可以通過從除數(shù)中減去1,然后把它和被除數(shù)進(jìn)行AND操作快速地獲得余數(shù)。然后把這個值加載到ECX寄存器中,執(zhí)行REP MOVSB指令組合來傳送剩余的字符。
可以看到,首先,在執(zhí)行REP MOVSL指令組合之后停止程序,顯示內(nèi)存位置buffer的內(nèi)容:
"This is a test of the conversion program"注意,前40個字符從源字符串傳送到了目標(biāo)字符串。下面,執(zhí)行REP MOVSB指令組合,并且再次查看內(nèi)存位置buffer的內(nèi)容:
"This is a test of the conversion program!\n"字符串中的最后兩個字符被成功地傳送了。
- 驗證實驗reptest4.s
向后執(zhí)行和向前執(zhí)行REP指令都是可以的。可以把DF標(biāo)志設(shè)置為對字符串進(jìn)行向后處理,按照相反的方向在內(nèi)存位置之間傳送它。
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o reptest4.o reptest4.s ld -m elf_i386 -o reptest4 reptest4.o gdb -q reptest4執(zhí)行結(jié)果如下:
分析:程序把源和目標(biāo)字符串的末尾加載到ESI和EDI寄存器中,然后使用STD指令設(shè)置DF標(biāo)志。這使目標(biāo)字符串按照相反的順序被存儲。
STOS指令
使用LODS指令把字符串值存放到EAX寄存器之后,可以使用STOS指令把它存放到另一個
內(nèi)存位置中。和LODS指令類似,根據(jù)要傳送的數(shù)據(jù)的數(shù)量,STOS指令有3種格式:
- STOSB: 存儲AL寄存器中一個字節(jié)的數(shù)據(jù)
- STOSW: 存儲AX寄存器中一個字(2字節(jié))的數(shù)據(jù)
- STOSL: 存儲EAX寄存器中一個雙字(4個字節(jié))的數(shù)據(jù)
STOS指令使用EDI寄存器作為隱含的目標(biāo)操作數(shù)。執(zhí)行STOS指令時,它按照使用的數(shù)據(jù)長
度遞增或者遞減EDI寄存器的值。
STOS指令可以和REP指令一起使用,多次把一個字符串值復(fù)制到大型字符串值中。
- 驗證實驗stostest1.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o stostest1.o stostest1.s ld -m elf_i386 -o stostest1 stostest1.o gdb -q stostest1執(zhí)行結(jié)果如下:
分析:程序吧ASCII空格字符加載到AL寄存器中,然后把它復(fù)制到buffer標(biāo)簽指向的內(nèi)存位置中256次。
可以看到,通過LODSB指令把空格字符加載到了AL寄存器中。在STOSB指令執(zhí)行之前,內(nèi)存位置buffer包含0。STOSB指令執(zhí)行之后,內(nèi)存位置buffer包含的都是空格。
構(gòu)建自己的字符串函數(shù)
STOS和LODS 指令可以用于各種字符串操作,通過使ESI和EDI寄存器指向相同的字符串,可以對字符串執(zhí)行簡單的操作。可以使用LODS指令遍歷字符串,一次把一個字符加載到AL寄存器中,對這個字符執(zhí)行某些操作,然后使用STOS指令把新的字符加載回字符串中。
- 驗證實驗convert.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o convert.o convert.s ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o convert -lc convert.o ./convert執(zhí)行結(jié)果如下:
分析:程序把ASCII字符串都轉(zhuǎn)換為大寫字母。
程序把內(nèi)存位置string1加載到ESI和EDI寄存器中,把字符串長度加載到ECX寄存器中。然后程序使用LOOP指令對字符串中的每個字符執(zhí)行字符檢查。程序進(jìn)行字符檢查的方法是,把每個字符加載到AL寄存器中,并且判斷它是否小于字母a的ASCII值(0x61),或者大于字母z的ASCII值(0x7a)。如果字符在這個范圍之內(nèi),那么它必然是小寫字母,必須通過減去0x20把它轉(zhuǎn)換為大寫字母。
不管是否對字符進(jìn)行了轉(zhuǎn)換,都必須把它存放回字符串,以便保持ESI和EDI寄存器的同步。對每個字符都運(yùn)行STOSB指令,然后代碼向后循環(huán)到下一個字符,直到完成字符串中所有字符的處理。
可以看到,確實所有的字母都被轉(zhuǎn)換成了大寫字母。
比較字符串
CMPS指令系列用于比較字符串值。和其他字符串指令一樣,CMPS指令有3種格式:
- CMPSB:比較字節(jié)值
- CMPSW:比較字(2字節(jié))值
- CMPSL:比較雙字(4字節(jié))值
和其他字符串指令一樣,隱含的源和目標(biāo)操作數(shù)的位置同樣存儲在ESI和EDI寄存器中。每次執(zhí)行CMPS指令時,根據(jù)DF標(biāo)志的設(shè)置,ESI和EDI寄存器按照被比較的數(shù)據(jù)的長度遞增或者遞減。
CMPS指令從源字符串中減去目標(biāo)字符串,并且適當(dāng)?shù)卦O(shè)置EFLAGS寄存器的進(jìn)位、符號、
溢出、零、奇偶校驗和輔助進(jìn)位標(biāo)志。CMPS指令執(zhí)行之后,可以根據(jù)字符串的值,使用一般的條件跳轉(zhuǎn)指令跳轉(zhuǎn)到分支。
- 驗證實驗cmpstest1.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o cmpstest1.o cmpstest1.s ld -m elf_i386 -o cmpstest1 cmpstest1.o ./cmpstest1 echo $?執(zhí)行結(jié)果如下:
分析:程序比較兩個字符串值,并且根據(jù)比較的結(jié)果設(shè)置程序的返回代碼。程序首先把
exit系統(tǒng)調(diào)用值加載到EAX寄存器中。把要測試的兩個字符串的位置加載到ESI和EDI寄存器中之后,程序使用CMPSL指令比較字符串的前4個字節(jié)。如果字符串相等,就使用JE指令跳轉(zhuǎn)到標(biāo)簽equal,這里把程序結(jié)果代碼設(shè)置為0并且退出。如果字符串不相等,則不會跳轉(zhuǎn)到分支,程序順序執(zhí)行,設(shè)置結(jié)果代碼為1并且退出。
可以看到,結(jié)果代碼為0,表示字符串互相匹配。
- 驗證實驗cmpstest2.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o cmpstest2.o cmpstest2.s ld -m elf_i386 -o cmpstest2 cmpstest2.o ./cmpstest2 echo $?執(zhí)行結(jié)果如下:
分析:程序把源和目標(biāo)字符串的位置加載到ESI和EDI寄存器中,把字符串長度加載到
ECX寄存器中。REPE CMPSB指令逐字節(jié)地重復(fù)字符串的比較,直到ECX寄存器為零,或者零標(biāo)志被設(shè)置,這表明不匹配。
REPE指令執(zhí)行之后,像以往那樣使JE指令檢查EFLAGS寄存器以便確定字符串是否相等。
如果REPE指令退出,則零標(biāo)志將被設(shè)置,JE指令不跳轉(zhuǎn)到分支,表示字符串不相同。ESI和EDI寄存器將包含字符串中不匹配字符的內(nèi)存位置,并且ECX寄存器將包含不匹配字符在字符串中的位置(從字符串的末尾向回計數(shù))。
可以看出,字符串比較是區(qū)分大小寫的。兩個字符串之間只有一個字符的大小寫有區(qū)
別,這會被比較程序檢測到。CMP指令從源字符串的十六進(jìn)制值中減去目標(biāo)字符串的值,得到結(jié)果11。
字符串不等
- 驗證實驗strcomp.s
程序定義兩個字符串string1和string2,還有它們的長度(length1和length2)。程序生成的結(jié)果代碼反映兩個字符串的比較情況:
| 255 | string1小于string2 |
| 0 | string1等于string2 |
| 1 | string1大于string2 |
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o strcomp.o strcomp.s ld -m elf_i386 -o strcomp strcomp.o ./strcomp echo $?執(zhí)行結(jié)果如下:
分析:從結(jié)果255可以看出,第一個字符串"test"小于第二個字符串"test1"。
掃描字符串
SCAS指令系列用于掃描字符串搜索一個或者多個字符。和其他字符串指令一樣,SCAS指
令有3個版本:
- SCASB:比較內(nèi)存中的一個字節(jié)和AL寄存器的值
- SCASW:比較內(nèi)存中的一個字和AX寄存器的值
- SCASL:比較內(nèi)存中的一個雙字和EAX寄存器的值
SCAS指令使用EDI寄存器作為隱含的目標(biāo)操作數(shù)。EDI寄存器必須包含要掃描的字符串的
內(nèi)存地址。和其他字符串指令一樣,當(dāng)執(zhí)行SCAS指令時,EDI寄存器的值按照搜索字符的數(shù)據(jù)長度遞增或者遞減(這取決于DF標(biāo)志的值)。
- 驗證實驗scastest1.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o scastest1.o scastest1.s ld -m elf_i386 -o scastest1 scastest1.o ./scastest1 echo $?執(zhí)行結(jié)果如下:
分析:可以看到,在字符串的第16個位置找到了"-"字符。
搜索多個字符
- 驗證實驗scastest2.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o scastest2.o scastest2.s ld -m elf_i386 -o scastest2 scastest2.o ./scastest2 echo $?執(zhí)行結(jié)果如下:
分析:程序試圖在字符串中查找字符序列“test"。它把整個搜索字符串加載到EAX寄存
器中,然后使用SCASL指令一次檢查字符串的4個字節(jié)。注意ECX寄存器沒有被設(shè)置為字符串的長度,而是被設(shè)置為REPNE指令遍歷整個字符串所需的迭代次數(shù)。因為每次迭代檢查4個字節(jié),所以ECX寄存器的值是整個字符串長度44的四分之一。
可以看到,結(jié)果代碼為0,說明SCASL指令在字符串中沒有找到字符序列"test"。顯然,出現(xiàn)了某些錯誤。
這是因為REPNE指令的第一次選代比較4個字節(jié)的"This"和EAX中的字符序列。因為它們不匹配,所以ECX寄存器遞增4,然后檢查下面的4個字節(jié)"is"。被測試的每一組4個字節(jié)都不和搜索字符序列相匹配,盡管這個序列確實在這個字符串中。
計算字符串的長度
- 驗證實驗strsize.s
程序的源代碼略。
執(zhí)行程序命令:
as --32 -gstabs -o strsize.o strsize.s ld -m elf_i386 -o strsize strsize.o ./strsize echo $?執(zhí)行結(jié)果如下:
分析:程序結(jié)果說明字符串的長度為35。
遇到問題
1.當(dāng)運(yùn)行某些程序時,如果需要進(jìn)行調(diào)試,按照單步執(zhí)行一步一步按s真的很慢,而且也容易因為按鍵太多而出一些錯誤。比如按照課本進(jìn)行單步調(diào)試aaatest.s時,需要進(jìn)行三四十步單步調(diào)試,才能退出中間的循環(huán),在程序結(jié)束前查看結(jié)果的值,比較復(fù)雜:
# aaatest.s - An example of using the AAA instruction .section .data value1:.byte 0x05, 0x02, 0x01, 0x08, 0x02 value2:.byte 0x03, 0x03, 0x09, 0x02, 0x05 .section .bss.lcomm sum, 6 .section .text .globl _start _start:nopxor %edi, %edimovl $5, %ecxclc loop1:movb value1(, %edi, 1), %aladcb value2(, %edi, 1), %alaaamovb %al, sum(, %edi, 1)inc %ediloop loop1adcb $0, sum(, %edi, 4)movl $1, %eaxmovl $0, %ebxint $0x80解決方案:在程序適當(dāng)?shù)牡胤皆O(shè)置斷點,通過cont命令使程序在適當(dāng)?shù)牡胤酵O聛?#xff0c;而不需要一次一次地手動去按s進(jìn)行單步調(diào)試:
# aaatest.s - An example of using the AAA instruction .section .data value1:.byte 0x05, 0x02, 0x01, 0x08, 0x02 value2:.byte 0x03, 0x03, 0x09, 0x02, 0x05 .section .bss.lcomm sum, 6 .section .text .globl _start _start:nopxor %edi, %edimovl $5, %ecxclc loop1:movb value1(, %edi, 1), %aladcb value2(, %edi, 1), %alaaamovb %al, sum(, %edi, 1)inc %ediloop loop1adcb $0, sum(, %edi, 4) end:movl $1, %eaxmovl $0, %ebxint $0x80在loop1和end處設(shè)置斷點,一個控制程序中間的循環(huán),使得循環(huán)還未結(jié)束時每次輸入cont都能在循環(huán)開始處停住,一個控制程序退出循環(huán)后,在結(jié)束程序前得到結(jié)果之后停住能夠查看結(jié)果。
可以看到,按鍵次數(shù)大大減少,無需按幾十次單步調(diào)試的命令才能完成對程序的調(diào)試和查看
總結(jié)
以上是生活随笔為你收集整理的操作系统实验报告4:Linux 下 x86 汇编语言3的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统实验报告3:Linux 下 x8
- 下一篇: 有关linux下redis overco