大多數人在高校裏面學的第一門彙編語言是基於16位的Intel 8086處理器(即8086彙編語言),現在的大多數系統都是32或者64位的,爲了實驗需要我們一般安裝DosBox來作爲16位DOS系統模擬器。
計算機類專業一般都會有編譯原理的課,課程會詳細介紹代碼編譯的各個階段:詞法分析、語法分析、語義分析、中間代碼產生、編譯優化與目標代碼生成。其中,在“目標代碼生成”階段,現代編譯器一般是生成一個彙編文件,博主想要獲取的就是基於16位 DOS系統的8086格式的彙編文件。
GCC與VS等主流的編譯器都提供了在編譯C/C++代碼過程中生成彙編指令的功能,詳細步驟非常簡單,在此不贅述。下面是C語言源代碼以及其通過VS 2019與GCC分別生成的彙編指令。
C語言源碼:
#include <stdio.h>
int main()
{
int i, j, n;
for (i = 1; i <= 9; i++)
{
for (j = 1; j <= i; j++)
printf("%d × %d = %2d ", i, j, i * j);
printf("\n");
}
return 0;
}
VS 2019生成彙編:
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.26.28806.0
TITLE c:\users\hadoop001\desktop\vs2019\main2\main2.c
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB OLDNAMES
PUBLIC ??_C@_0BB@KPGCELHG@?$CFd?5?C?$JH?5?$CFd?5?$DN?5?$CF2d?5?5@ ; `string'
PUBLIC ??_C@_01EEMJAFIK@?6@ ; `string'
EXTRN __imp____stdio_common_vfprintf:PROC
EXTRN __imp____acrt_iob_func:PROC
EXTRN @__security_check_cookie@4:PROC
; COMDAT ??_C@_01EEMJAFIK@?6@
CONST SEGMENT
??_C@_01EEMJAFIK@?6@ DB 0aH, 00H ; `string'
CONST ENDS
; COMDAT ??_C@_0BB@KPGCELHG@?$CFd?5?C?$JH?5?$CFd?5?$DN?5?$CF2d?5?5@
CONST SEGMENT
??_C@_0BB@KPGCELHG@?$CFd?5?C?$JH?5?$CFd?5?$DN?5?$CF2d?5?5@ DB '%d ', 0c3H
DB 097H, ' %d = %2d ', 00H ; `string'
CONST ENDS
PUBLIC _main
PUBLIC _printf
PUBLIC __vfprintf_l
PUBLIC ___local_stdio_printf_options
COMM ?_OptionsStorage@?1??__local_stdio_printf_options@@9@9:QWORD ; `__local_stdio_printf_options'::`2'::_OptionsStorage
_DATA ENDS
; Function compile flags: /Ogtp
; File C:\Users\hadoop001\Desktop\VS2019\main2\main2.c
; COMDAT _main
_TEXT SEGMENT
_main PROC ; COMDAT
; 5 : {
push ebx
push esi
push edi
; 6 : int i, j, n;
; 7 : for (i = 1; i <= 9; i++)
mov esi, 1
$LL4@main:
; 8 : {
; 9 : for (j = 1; j <= i; j++)
mov edi, 1
mov ebx, esi
npad 1
$LL7@main:
; 10 : printf("%d × %d = %2d ", i, j, i * j);
push ebx
push edi
push esi
push OFFSET ??_C@_0BB@KPGCELHG@?$CFd?5?C?$JH?5?$CFd?5?$DN?5?$CF2d?5?5@
call _printf
inc edi
add esp, 16 ; 00000010H
add ebx, esi
cmp edi, esi
jle SHORT $LL7@main
; 11 : printf("\n");
push OFFSET ??_C@_01EEMJAFIK@?6@
call _printf
inc esi
add esp, 4
cmp esi, 9
jle SHORT $LL4@main
; 12 : }
; 13 :
; 14 : return 0;
pop edi
pop esi
xor eax, eax
pop ebx
; 15 : }
ret 0
_main ENDP
_TEXT ENDS
END
GCC生成彙編:
.file "main1.c"
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "%d \303\227 %d = %2d \0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $48, %rsp
.seh_stackalloc 48
.seh_endprologue
call __main
movl $1, -4(%rbp)
jmp .L2
.L5:
movl $1, -8(%rbp)
jmp .L3
.L4:
movl -4(%rbp), %eax
imull -8(%rbp), %eax
movl %eax, %edx
movl -8(%rbp), %ecx
movl -4(%rbp), %eax
movl %edx, %r9d
movl %ecx, %r8d
movl %eax, %edx
leaq .LC0(%rip), %rcx
call printf
incl -8(%rbp)
.L3:
movl -8(%rbp), %eax
cmpl -4(%rbp), %eax
jle .L4
movl $10, %ecx
call putchar
incl -4(%rbp)
.L2:
cmpl $9, -4(%rbp)
jle .L5
movl $0, %eax
addq $48, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (GNU) 10.1.0"
.def printf; .scl 2; .type 32; .endef
.def putchar; .scl 2; .type 32; .endef
可能是先入爲主,我一直好奇:8086彙編表示的目標代碼會是什麼樣子的?之前也試了不少方法,均告失敗,昨天一頓操作居然成功了。首先安裝DosBox,之後下載TCC 2.0 for DOS(TCC 2.0 for DOS下載地址(侵刪!)),將解壓後的tc文件夾放在DosBox的安裝目錄下,如圖1所示。
爲了之後能夠操作成功,建議對tc/include下的stdio.h文件做如下修改:
如圖2所示,即將tc/include/stdio.h第30行的
#include <stdarg.h>
改爲
#include "include/stdarg.h"
因爲上面的代碼只包含了stdio.h頭文件,因此博主只針對包含stdio.h會產生的問題進行了解決。(可能還有其他坑,哈哈哈!)
修改文件完成後,打開DosBox,進入tc文件夾,運行tcc.exe將C程序彙編爲16位的8086彙編指令。在這之前需先將C文件放置在與tcc.exe同一目錄下。執行的過程與結果如圖3至圖6所示。
用到的命令總結如下:
tcc -S -ml main1.c
copy main1.asm .
cd ..
masm main1.asm
link main1.obj
最終得到的彙編代碼如下所示:
ifndef ??version
?debug macro
endm
endif
?debug S "main1.c"
MAIN1_TEXT segment byte public 'CODE'
DGROUP group _DATA,_BSS
assume cs:MAIN1_TEXT,ds:DGROUP
MAIN1_TEXT ends
_DATA segment word public 'DATA'
d@ label byte
d@w label word
_DATA ends
_BSS segment word public 'BSS'
b@ label byte
b@w label word
?debug C E95359CD50076D61696E312E63
?debug C E9B434CD500F696E636C7564652F737464696F2E68
?debug C E900501D1110696E636C7564652F7374646172672E68
_BSS ends
MAIN1_TEXT segment byte public 'CODE'
; ?debug L 3
_main proc far
push si
push di
; ?debug L 6
mov si,1
jmp short @5
@4:
; ?debug L 8
mov di,1
jmp short @9
@8:
; ?debug L 9
mov ax,si
mul di
push ax
push di
push si
push ds
mov ax,offset DGROUP:s@
push ax
call far ptr _printf
add sp,10
@7:
inc di
@9:
cmp di,si
jle @8
@6:
; ?debug L 10
push ds
mov ax,offset DGROUP:s@+17
push ax
call far ptr _printf
pop cx
pop cx
@3:
inc si
@5:
cmp si,9
jle @4
@2:
; ?debug L 13
xor ax,ax
jmp short @1
@1:
; ?debug L 14
pop di
pop si
ret
_main endp
MAIN1_TEXT ends
?debug C E9
_DATA segment word public 'DATA'
s@ label byte
db 37
db 100
db 32
db 195
db 151
db 32
db 37
db 100
db 32
db 61
db 32
db 37
db 50
db 100
db 32
db 32
db 0
db 10
db 0
_DATA ends
extrn _printf:far
MAIN1_TEXT segment byte public 'CODE'
MAIN1_TEXT ends
public _main
end
從圖5與圖6可以看出,TCC所生成的8086彙編代碼可以通過MASM的彙編,但無法通過LINK的鏈接,原因是因爲C源程序中調用了庫函數printf,其在最終生成可執行文件時纔會被OS調用相應庫函數的機器指令所“填充”,目標代碼等於只是做了一個預調用的“標記”。