Advertisement

《汇编语言》- 读书笔记 - 综合研究

阅读量:

《汇编语言》- 读书笔记 - 综合研究

  • 研究试验 1 为本研究设计一个简洁的C开发环境

    • 首先进行获取
    • 接下来进行设置
    • 然后依次执行编译与链接操作
    • 最后通过手动方式完成程序链接
    • 具体来说:
      • 使用tcc对多个源文件执行编译与链接过程
      • 并通过手动方式完成程序链接
  • 在本次试验中采用寄存器完成相关操作

  • 编写一个名为ur1.c的程序

  • 通过调试工具加载ur1.exe并获取其实现细节

  • 采用以下方法获取ur1.exe运行时main函数在代码段中的偏移地址信息

  • 再次使用调试工具打开ur1.exe文件后端查看main函数的具体汇编表示

  • 观察到main函数后存在ret指令这一特性推测C语言中的函数实现会在汇编层次上作为子过程进行处理

  • 进一步分析以下汇编代码段落以验证上述推测成立

  • 进行研究试验3时会占用内存空间

  • 编写一个名为um1.c的程序

  • 编写一个名为um1.c的程序,在屏幕上显示一个绿色字符a

  • 对以下程序中的所有函数进行汇编代码分析,并思考相关问题

  • 请问C语言中的全局变量存储在什么地方?

  • 请问局部变量在C语言中是如何存储的?

  • 在C语言中,函数开始处的指令push bp mov bp sp具体代表什么操作?

  • 栈帧(Stack Frame)

  • 4. 研究汇编代码的相关问题。

  • 5. 程序按序填充内存区域中的8个字符' a'至'h' ,深入掌握内存管理知识。

  • 研究试验4避免使用主函数进行编程

    • 1. 编译并连接这段代码后进行相关问题的思考
    • 1. 在编译或连接过程中哪些环节可能会出现问题?
      • 2. 出现的错误提示信息具体是什么?
      • 3. 这个错误提示可能与哪个文件有关?
  • 使用学习编程语言中常用的链接工具 link.exe 对 tc.exe 生成的目标文件 f.obj 实施连接操作,从而得到最终可执行文件 f.exe. 然后通过调试器加载该程序,并查看其完整的汇编代码.

    • 问题如下:
      • 该程序代码的总字节数是多少?
      • 该程序能否正确返回结果?
      • 该函数的偏移地址具体是多少?
  • 3. 写一个程序 m.c

    • 1. 请计算m.exe程序代码的总字节数。
    • 2. 请确认m.exe是否能够正确返回。
    • 3. 请比较分析以下两个汇编函数:一个是位于文件中主函数部分的m.exe主函数代码段,另一个是位于f.exe文件中的f函数代码段。
    • 4. 使用调试工具对m.exe进行跟踪分析。
    1. 思考以下几个问题:
      1. main函数调用指令与程序返回指令的来源是什么?
    1. 在没有main函数的情况下, 错误信息中包含与cos相关的信息;而在构建开发环境时, 没有cos.obj文件中的tc.exe无法连接程序. 是否tc.exe将cos.obj文件与用户程序的.obj文件一起连接生成.exe文件?
    1. 用户程序中的main函数被调用指令与其返回指令是否均来自cos.obj文件?
    1. 我们如何查看cos.obj文件中的程序代码?
    1. cos.obj文件中包含我们所设想的代码吗?
    1. 使用link.exe将c:\minic(我的在c:\TC20\LIB)目录下的cos.obj连接,生成cos.exe.
    1. 利用Debug工具, 查找m.exe中调用main函数的call指令偏移地址,从该偏移地址开始向后查看10条指令;然后使用Debug加载cos.exe,从相同偏移地址开始向后查看10条指令. 对比两处指令序列.
    1. Turbo C编译器作为编译器连接器,将cos.obj文件与用户编写的.obj文件链接生成.exe文件的过程及其内部运行机制大致如下:
    1. 使用tc.exe重新编译f.c源代码,完成连接并生成f.exe.
    1. 在基于新cos.obj的基础上,编写一个新的f.c源代码,将其安全地写入内存空间中的a到h八个字符之间运行分析理解f.c的行为.

参数

  • 本节实验:探讨函数参数接收机制
      • 分析程序 a.c
        • 分析程序 b.c
        • 开发一个简单的 printf 模拟函数
        • 只需支持 %c%d 格式化输出
        • myprintf.asm 实现
        • main.c 程序框架
          • 测试 %c 格式字符输出
          • 测试 %d 格式字符无指定宽度显示
          • 测试 %d 格式字符指定宽度显示
          • 测试 %d 格式字符单独指定宽度显示(使用【参数
    • 总结

      • 反汇编分析 C
    • 参考资料

研究试验 1 搭建一个精简的 C 语言开发环境

我不愿意花冤枉钱去简化它;所以直接下一步就准备安装一个绿色版本系统了;只要它能正常运行即可。(作者在提取精简版本的过程中熟悉了几个关键文件;这有助于后续讨论的内容展开)

1. 下载

Turbo C 2.0 dosbox绿色版本适用于Windows 7 64-bit
喜欢安装包的可以了解:Turbo C 2.0安装及使用说明

2. 配置

我的工作目录位于E:\c文件夹中;我将TC20文件夹直接复制到该位置。
使用预设好的快捷方式目标:运行命令E:\DOSBox\DOSBox.exe -conf "dosbox-for_TC20.conf" -noconsole

复制代码
    [autoexec]
    # Lines in this section will be run at startup.
    # You can put your MOUNT lines here.
    mount c E:\c
    set PATH=%PATH%;c:\TC20;
    c:
    # 挂载软盘镜像为 A 盘
    imgmount A E:\c\A.flp -t floppy -fs fat -size 1440
在这里插入图片描述

如果 TC20文件夹改了名,使用时找不到东西会报错,这里要自己改一下。

在这里插入图片描述

3. 编译 Compile

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. 构建 Make

自动编译、连接得到 exe

在这里插入图片描述
在这里插入图片描述

6. 手动编译连接

尽管 TC 为整个开发环境提供了必要的支持,但相较于传统的 DOS 虚拟机(上古IDE),我更倾向于在 Windows 系统上依赖于自己熟悉且功能强大的编辑器进行编程操作。

操作 命令行 输出
编译连接 tcc demo.c DEMO.OBJ, DEMO.EXE
编译连接:自定义EXE 名称 tcc -eAAA.exe demo.c DEMO.OBJ, AAA.EXE
只编译 tcc -c demo.c DEMO.OBJ
只编译:自定义OBJ 名称 tcc -c -oBBB.obj demo.c BBB.OBJ
输出汇编 tcc -S demo.c DEMO.ASM

tcc 编译连接多个源文件

  • main.c
复制代码
    int add(int a, int b);
    
    void main() {
    	int c = add(1, 2);
    	printf("hello world:%d", c);
    }
  • add.c
复制代码
    int add(int a, int b) {
    	return a + b;
    }

编译连接指令为:tcc -edemo.exe main.c add.c 这里设置生成文件名为 demo.exe

在这里插入图片描述

语法:多个obj文件、exe文件、地图文件、库文件
编译连接命令:tlink c:\tc20\lib\c0s.obj main对象和附加对象, main.exe,,c:\tc20\lib\库
命令中 c:\tc20 是TC的安装位置,请根据个人情况自行调整

在这里插入图片描述

关于编译模式有:T微、S小、C紧、M中、L大、H巨,分别对应:

在这里插入图片描述

常规设置下,默认采用小模式;用户可以根据需求自行选择连接方式。

更换其他模式后,在编译过程中,请确保使用相应的选项:-ms,-mc,-mm,-ml,-mh。

  1. T模式与S共享相同的lib文件。
  2. 其(T模式编译生成的)exe可以通过命令转换成com格式。

例如通过命令 tcc -mt -lt demo.c 即可生成相应的 demo 文件

各模式与启动文件库文件的对应关系:

模式 启动文件 数学库 turbo c 标准运行库
Tiny C0T.OB MATHS.LIB CS.LIB
Small(默认) C0T.OB MATHS.LIB CS.LIB
Compact C0C.OBJ MATHC.LIB CC.LIB
Medium C0M.OBJ MATHM.LIB CM.LIB
Large C0L.OBJ MATHL.LIB CL.LIB
Huge C0H.OBJ MATHH.LIB CH.LIB

研究试验 2 使用寄存器

1. 编一个程序 ur1.c

编译,连接,生成 ur1.exe

复制代码
    main()
    {
    	_AX=1;
    	_BX=1;
    	_CX=2;
    	_AX=_BX+_CX;
    	_AH=_BL+_CL;
    	_AL=_BH+_CH;
    }
复制代码
    tcc -c ur1.c
    tlink c:\tc20\lib\c0s ur1, ur1,,c:\tc20\lib\cs

2.用 Debug 加载 ur1.exe,用u命令査看 ur1.c 编译后的机器码和汇编代码

这个区域属于启动代码的部分,在s模式下进行操作的方法入口地址为'1fa'(下一条会介绍获取方法)。

在这里插入图片描述

按照以下方式获取正在被加载运行时的ur1.exe的主要函数在代码段中的偏移地址:

复制代码
    main()
    {
    	printf("%x\n",main);
    }

"%x\n"的作用是将变量以十六进制格式输出。\n\n这个程序为何能够在代码段中打印出main函数的偏移地址?

回答:main函数的名称即为它的入口偏移位置。通过 printf 输出的是 main函数的入口偏移位置 1fa。

复制代码
    ...
    _main	proc	near
    	mov	ax,offset _main
    	push	ax
    	mov	ax,offset DGROUP:s@
    	push	ax
    	call	near ptr _printf
    	pop	cx
    	pop	cx
    @1:
    	ret	
    _main	endp
    ...

通过启动调试器加载ur1.exe,并根据上述main函数的偏移地址输出结果进行分析。然后使用u命令观察main函数的汇编代码。最后详细分析ur1.c源代码中的每一条C语句对应生成的机器指令

在这里插入图片描述

在 main 函数之后紧跟 ret 指令这一现象提示我们:C 语言可能将函数以子程序的形式实现于汇编代码中。为了进一步探讨这一特性,请分析并研究下列汇编代码,并以验证我们的假设。

复制代码
    void f(void);
    main()
    {
    	_AX=1; _BX=1; _CX=2;
    	f();
    }
    
    void f(void){
    	_AX=_BX+_CX;
    }
在这里插入图片描述

tcc -S ur2.c 输出汇编代码,可以很清晰的看到两个子程序 _main_f

复制代码
    ...
    _TEXT	segment	byte public 'CODE'
    _main	proc	near
    	mov	ax,1
    	mov	bx,1
    	mov	cx,2
    	call	near ptr _f
    @1:
    	ret	
    _main	endp
    
    _f	proc	near
    	mov	ax,bx
    	add	ax,cx
    @2:
    	ret	
    _f	endp
    _TEXT	ends
    ...

研究试验 3 使用内存空间

复制代码
    *(char *)0x2000 = 'a';			// mov byte ptr [2000], 'a'
    
    *(char far *)0x20001000='a';	// mov bx,2000h
    								// mov es,bx
    								// mov bx,1000h
    								// mov byte ptr es:[bx],'a'

语法分析

0x2000 是一个十六进制数,并标识一个特定的内存地址。
* char * 用于表示字符类型的指针变量。
* (char *) 运算涉及将数值转换为指向该数值的字符类型的指针。
* 解引用操作符用于访问指向内存位置的指针所存储的内容。
如果变量 c 中存在指向 bx 的指针 p(bx代表内存地址),那么:
通过 *p 可以访问与 bx 相同内存地址的内容。

  • 扩展:变量\texttt{a}被定义为字符指针类型,
    另一种方式是定义名为\texttt{a}的指针变量,
    两者在C语言中功能相同,
    编译器遵循一致的规则处理它们

含义

在C语言中,
指针作为一个标识符,
其存储的位置是计算机内部的一个随机访问单元。
通过运算符 * 解析后,
其结果就是该存储单元中的数据内容。
整个表达式 (char *) 表示将指定的十六进制数视为一个可操作的单字节空间。
其中,
*(char *) 表示对该空间进行赋值的操作,
具体来说,
这条语句的作用就是将单字节字符 'a' 存储到指定内存位置。
这种操作意味着程序直接向指定内存位置赋值而不涉及中间变量间接操作的过程。

使用场景

  • 直接处理内存操作:这类表达式常见于对硬件进行直接控制或与特定内存区域交互的低级编程情境中。
  • 静态地管理内存空间:当开发者预先知道某个地址块可用于程序运行时(虽然这并不是C语言的标准做法),他们可能会对该地址块赋值。然而,在现代操作系统环境中未预先分配该地址块的空间进行无序写入可能会导致段错误(segmentation fault)或其他不稳定行为。

需要注意的是,在实际编程中对这样的内存地址进行处理时应当谨慎 为了确保该内存地址确实已经被分配并用于程序运行 必须先确认该内存空间的有效性 在大多数现代系统中 非专用场景下对任意内存地址进行直接写入存在安全隐患 只有在内核模式运行或使用专门的安全API时才允许进行此类操作

1. 编一个程序 um1.c

复制代码
    main()
    {
    	*(char *)0x2000='a';
    	*(int *)0x2000=0xF;
    	*(char far *)0x20001000='a';
    	
    	_AX=0x2000;
    	*(char *)_AX='b';
    	
    	_BX=0x1000;
    	*(char *)(_BX+_BX)='a';
    	*(char far *)(0x20001000+_BX)=*(char *)_AX;
    }
  • 第一句:*(char *)0x2000='a';DS:2000 处写入了字符 a
在这里插入图片描述
  • 第三句:*(char far *)0x20001000='a';2000:1000 处写入了字符 a
在这里插入图片描述

2. 编一个程序,用一条C语句实现在屏幕的中间显示一个绿色的字符 a

复制代码
    main(){ 
    	*(int far *)0xb80007d0=0x0261;
    }
在这里插入图片描述

3. 分析下面程序中所有函数的汇编代码,思考相关的问题

为了接下来方便分析反汇编,这里我将书中的16进制数换成10进制更直观点。

复制代码
    int a1, a2, a3;
    
    void f(void);
    
    main() {
    int b1, b2, b3;
    a1 = 161; a2 = 162; a3 = 163;
    b1 = 177; b2 = 178; b3 = 179;
    }
    
    void f(void) {
    int c1, c2, c3;
    a1 = 4001; a2 = 4002; a3 = 4003;
    c1 = 193;  c2 = 194;  c3 = 195;
    }

TCC 输出反汇编:

复制代码
    	ifndef	??version
    ?debug	macro
    	endm
    	endif
    	?debug	S "um2.c"
    	
    _TEXT	segment	byte public 'CODE'
    DGROUP	group	_DATA,_BSS
    	assume	cs:_TEXT,ds:DGROUP,ss:DGROUP
    _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 E9F5568B5805756D322E63
    _BSS	ends
    
    ; ================= 代码段 ================
    _TEXT	segment	byte public 'CODE'
    ; --------------- main 函数 ---------------
    _main	proc	near
    
    	push	bp								; 保存调用方的栈帧基址
    	mov	bp,sp								; 将栈顶SP,传给当前栈帧基址
    	
    	sub	sp,6								; 在当前栈帧开辟6个字节的空间
    											; 对应 int b1,b2,b3;
    
    	mov	word ptr DGROUP:_a1,161				; a1=161
    	mov	word ptr DGROUP:_a2,162     		; a2=162
    	mov	word ptr DGROUP:_a3,163     		; a3=163
    	
    	mov	word ptr [bp-6],177					; b1=177
    	mov	word ptr [bp-4],178         		; b2=178
    	mov	word ptr [bp-2],179         		; b3=179
    @1:
    	mov	sp,bp								; 将(当前栈帧基址)恢复到SP,收缩栈空间
    	pop	bp									; 恢复调用者栈帧基址
    	ret										; 返回调用者
    _main	endp
    ; ------------------------------------------
    ; 子程序 _f 对应函数 void f(void)
    ; --------------- f(void) 函数 -------------
    _f	proc	near
    	push	bp
    	mov	bp,sp
    	sub	sp,6
    
    	mov	word ptr DGROUP:_a1,4001			; a1=4001
    	mov	word ptr DGROUP:_a2,4002    		; a2=4002
    	mov	word ptr DGROUP:_a3,4003    		; a3=4003
    	                                
    	mov	word ptr [bp-6],193         		; c1=193
    	mov	word ptr [bp-4],194         		; c2=194
    	mov	word ptr [bp-2],195         		; c3=195
    @2:
    	mov	sp,bp
    	pop	bp
    	ret	
    _f	endp
    _TEXT	ends
    ; ================== 代码段 ================
    
    ; ============= 未初始化的数据段 ===========
    ; 全局变量
    ; 在8086环境下,C语言中的int类型通常占据2个字节(16位)
    ; -----------------------------------------
    _BSS	segment word public 'BSS'
    _a1	label	word							; 定义标号 _a1 类型为 word,对应 int a
    	db	2 dup (?)							; 分配 2 字节空间,并未初始化
    _a2	label	word							; int b
    	db	2 dup (?)
    _a3	label	word							; int c
    	db	2 dup (?)
    _BSS	ends
    ; ============ 未初始化的数据段 ============
    
    	?debug	C E9
    _DATA	segment word public 'DATA'
    s@	label	byte
    _DATA	ends
    
    _TEXT	segment	byte public 'CODE'
    _TEXT	ends
    
    	; 这些都被声明为公开可以被外部访问了
    	public	_main
    	public	_f
    	public	_a3
    	public	_a2
    	public	_a1
    	end

1. C语言将全局变量存放在哪里?

回答:全局变量被定义在未初始化的数据段(BSS段)中。此外,在最后还会将它们指定为 public

2. 将局部变量存放在哪里?

回答:局部变量存储于当前栈帧。(在堆栈中为当前函数开辟出的一段独立区域)

3. 每个函数开头的 push bp mov bp sp 有何含义?

答:这两条指令共同完成了函数栈帧的初始化,并且能够保证在函数执行过程中能够独立地进行栈空间管理,并且也不会影响其他栈帧

  • 在基址寄存器bp中存储了被调用程序的栈基地址。
    • 通过指令push bp将被调用程序的栈基地址压入堆栈。
    • 使用mov指令将sp中的当前值赋给bp。

在函数返回时,还有对应的指令恢复原栈帧返回调用者。

复制代码
    mov sp, bp  	; 将BP的值(当前栈帧基址)恢复到SP,收缩栈空间
    pop bp      	; 从栈顶弹出先前保存的调用者栈帧基址,恢复BP的值
    ret         	; 从栈顶弹出返回地址,并跳转到该地址继续执行(返回调用者)

在执行一系列操作后,在程序执行过程中实现了对函数调用栈帧的生成、调用和释放功能。这不仅确保了函数调用的逻辑正确性与安全性,并且有效优化了系统资源利用效率。

栈帧(Stack Frame)

Stack Frame is a data structure created during program execution to support function calls. It primarily resides on the program's call stack (Call Stack). Each function call generates an independent stack frame to store:

  1. Real Parameters: The actual values passed to the function when it is invoked.
  2. Internal Variables: Variables declared within the function, which are only accessible during its execution.
  3. Return Address: The address of the next instruction to be executed after the function completes.
  4. Preceding Stack Frame Base Address (in certain architectures): A value used to recover the context of the caller's stack frame.

每当发生函数调用时(即每当执行一条包含函数调用指令的指令),都会生成一个新的栈帧;当该函数完成执行并返回控制权给调用者后(即当一条函数返回指令被执行),该栈帧会被销毁不再占用内存空间。这些栈帧按照从上到下的顺序依次排列于调用栈中,并呈现出一种层次化的结构布局——这种层次化结构反映了程序执行过程中各个子程序之间的嵌套关系。一般来说,在编译过程中会被预先分配好大小(除非采用动态内存分配的方式),并且各个栈帧之间紧密相连,在内存布局上便于快速访问和操作。

4. 分析下面程序的汇编代码,思考相关的问题。

复制代码
    int f(void);
    
    int a,b,ab;
    
    main() {
    int c;
    c=f();
    }
    
    int f(void) {
    	ab=a+b;
    	return ab;
    }

问题:C语言将函数的返回值存放在哪里?

复制代码
    ...
    _TEXT	segment	byte public 'CODE'
    _main	proc	near
    	push	bp
    	mov	bp,sp
    	sub	sp,2
    
    	call	near ptr _f					; 调用 f()
    	mov	word ptr [bp-2],ax				; 从 ax 拿返回值
    @1:
    	mov	sp,bp
    	pop	bp
    	ret	
    _main	endp
    
    _f	proc	near
    	mov	ax,word ptr DGROUP:_a
    	add	ax,word ptr DGROUP:_b
    	mov	word ptr DGROUP:_ab,ax
    
    	mov	ax,word ptr DGROUP:_ab			; 返回值放到 ax 中
    	jmp	short @2
    @2:
    	ret	
    _f	endp
    _TEXT	ends
    ...

ax 不够用的情况 :

复制代码
    _f	proc	near
    	mov	dx,word ptr DGROUP:_a+2
    	mov	ax,word ptr DGROUP:_a
    	add	ax,word ptr DGROUP:_b
    	adc	dx,word ptr DGROUP:_b+2
    	mov	word ptr DGROUP:_ab+2,dx
    	mov	word ptr DGROUP:_ab,ax
    
    	mov	dx,word ptr DGROUP:_ab+2		; 高16位
    	mov	ax,word ptr DGROUP:_ab			; 低16位
    	jmp	short @2
    @2:
    	ret	
    _f	endp

答: ax,如果 ax 不够用还会拉上 dx高16位存DX低16位放AX

本段代码中将从字母'a'到'h'依次写入八个字符,并要求透彻了解该程序的作用以及全面掌握相关技术细节。

复制代码
    // 定义宏 Buffer 它是一个指向 0:200h 的字符指针,内存单元 0:200h 中保存的就是字符指针的值
    #define Buffer ((char *)*(int far *)0x200) 
    
    main()
    {
    	// 使用 malloc 函数为 Buffer 分配一块大小为20字节的动态内存。
    	// malloc 返回的指针(类型为void *)要强制转换为 char * 才赋值给Buffer。
    	// 现在 Buffer 指向一块可写、可读的连续字符数组,长度为20字节。
    	Buffer=(char *)malloc(20);
    	// 将 Buffer 数组的第11个元素(索引为10,因为数组索引从0开始)初始化为整数值 0
    	// 接下来的 while 循环会用它计数。类似 for 循环中的 i
    	Buffer[10]=0;
    	// 循环条件 Buffer[10] != 8 就继续,每次 Buffer[10]自增 1。从 0 到 7 共 8 次
    	// 从 Buffer[0] 开始存入 'a' + Buffer[10] 的结果,
    	// 计算时用字符的ascii码 97 与 Buffer[10] 相加。 97 到 104 正好是 a 到 h, 
    	// 'a' + 0 = 'a'
    	// ...
    	// 'a' + 7 = 'h'
    	while(Buffer[10]!=8){
    		Buffer[Buffer[10]]='a'+Buffer[10];
    		Buffer[10]++;
    	}
    	// 调用 free 函数释放之前为 Buffer 分配的动态内存
    	free(Buffer);
    }

分析:

该预处理指令用于制定一个宏名称,并遵循指定的格式规则。其形式包括使用#define 宏名 宏替换内容的语法结构。

  • 首先指出,在内存地址0x200处存储了一个指向该地址的长整型远指针。
  • 接着说明,在编程语言中使用星号运算符对这个远指针进行解引用操作,从而获取对应的整数值(int)
  • 然后指出,在程序流程中将该整数值强制转换为字符型变量的形式,并将其存储在字符型指针变量(char*)中。
  • 综上所述,在这段代码中定义了一个字符型变量Buffer,并将其赋值为内存地址0x200处的内容。

函数 malloc() 是C语言中的一个基础库函数,在动态内存管理中发挥着关键作用。它被广泛用于动态内存空间的分配任务,并常见于包含在stdlib.h标准头文件中以实现相关功能。

复制代码
 * **功能:** `malloc()` 函数的主要功能是在程序运行时`动态`地为程序`分配`一块指定大小的`内存`块。
 * **返回值:** `malloc()` 函数返回一个指向所分配内存块起始位置的`指针`。  

如果分配成功,返回的指针类型为void *,这意味着它可以被转换为任何类型的指针(例如,int *、char *、struct MyStruct *等),以符合实际内存块的用途。
如果内存分配失败(例如,系统资源不足或请求的内存过大)返回 NULL

  • 反汇编分析
复制代码
    	...略
    _TEXT	segment	byte public 'CODE'
    _main	proc	near
    	; Buffer=(char *)malloc(20);
    	mov	ax,20					
    	push	ax					; 20压栈作为 malloc 参数
    	call	near ptr _malloc	; 调用 malloc 申请 20 字节的内存空间
    	pop	cx						; 弹出调用函数前被压入栈中的参数 20
    	xor	bx,bx					; 清零 bx
    	mov	es,bx					; 设置段地址
    	mov	bx,512					; 设置偏移地址
    									; 这里的 512 就是 Buffer 宏定义时的 0x200
    	mov	word ptr es:[bx],ax		; word ptr es:[bx] 就是 Buffer 指针对应的内存位置
    									; ax 就是将 malloc 返回的值
    	; --------------------------
    	; 这4句固定搭配,拿到 Buffer
    	; 其实就是算出它的偏移量给 bx
    	; --------------------------
    	xor	bx,bx
    	mov	es,bx
    	mov	bx,512
    	mov	bx,word ptr es:[bx]		; bx 再在就是动态分配的那 20 字节空间的首地址偏移
    	
    	; --------------------------
    	; 这句对应 Buffer[10]=0;
    	; byte ptr [bx]    就是 Buffer
    	; byte ptr [bx+10] 就是 Buffer[10]
    	; --------------------------
    	mov	byte ptr [bx+10],0
    
    	jmp	short @2				; @4 后面是循环体,开始前先跳 @2 那里是循环条件
    @4:
    	; --------------------------
    	; Buffer[Buffer[10]]='a'+Buffer[10]; 的 = 号右边:
    	; 这4句固定搭配,拿到 Buffer
    	; --------------------------
    	xor	bx,bx
    	mov	es,bx
    	mov	bx,512
    	mov	bx,word ptr es:[bx]
    	
    	; --------------------------
    	; 'a'+Buffer[10]
    	; --------------------------
    	mov	al,byte ptr [bx+10]
    	add	al,97
    	
    	; --------------------------
    	; Buffer[Buffer[10]]='a'+Buffer[10]; 的 = 号左边:
    	; 这4句固定搭配,拿到 Buffer
    	; --------------------------
    	xor	bx,bx
    	mov	es,bx
    	mov	bx,512
    	mov	bx,word ptr es:[bx]
    	
    	push	ax					; al 里存着 'a'+Buffer[10] 的结果
    	push	bx					; bx 是 Buffer 的偏移量
    	xor	bx,bx					; 清零
    	
    	; --------------------------
    	; 拿到 Buffer[10] 的值存在 ax (后面用它当索引)
    	; --------------------------
    	mov	es,bx
    	mov	bx,512
    	mov	bx,word ptr es:[bx]
    	mov	al,byte ptr [bx+10]
    	cbw							; 将 AL 扩展为 AX 符号不变。
    	
    	pop	bx						; 取出 Buffer 的偏移量
    	add	bx,ax					; Buffer[ax] 也就是 Buffer[Buffer[10]]
    	
    	; --------------------------
    	; 将等号右边的结果赋值给 Buffer[Buffer[10]]
    	; --------------------------
    	pop	ax						; 恢复 ax (等号右边的结果)
    	mov	byte ptr [bx],al 		; 赋值
    
    	; --------------------------
    	; 这4句固定搭配,拿到 Buffer
    	; --------------------------
    	xor	bx,bx
    	mov	es,bx
    	mov	bx,512
    	mov	bx,word ptr es:[bx]
    	
    	inc	byte ptr [bx+10]		; 对应 Buffer[10]++;
    @2:
    	; --------------------------
    	; 这4句固定搭配,拿到 Buffer
    	; --------------------------
    	xor	bx,bx
    	mov	es,bx
    	mov	bx,512
    	mov	bx,word ptr es:[bx]
    	
    	cmp	byte ptr [bx+10],8		; 如果 Buffer[10]!=8
    	jne	@4						; 继续循环
    @3:
    	; --------------------------
    	; 这4句拿到 Buffer 压栈作为 free 的参数
    	; --------------------------
    	xor	bx,bx
    	mov	es,bx
    	mov	bx,512
    	push	word ptr es:[bx]
    	
    	call	near ptr _free		; 对应 free(Buffer);
    	pop	cx						; 清理 调用 free 压的参数
    @1:
    	ret	
    _main	endp
    _TEXT	ends
    	...略
在这里插入图片描述

注意:在内存未被释放之前进行检查是为了确保数据完整性。
当内存被释放后可能会被其他程序修改。

研究试验 4 不用 main 函数编程

1. 编译,连接这段代码思考相关问题

复制代码
    f()
    {
    	*(char far *)(0xb8000000+160*10+80)='a';
    	*(char far *)(0xb8000000+160*10+81)=2;
    }

1. 编译和连接哪个环节会出问题?

答: 编译成功,连接失败。

2. 显示出的错误信息是什么?

答: 没定义 main

在这里插入图片描述

3. 这个错误信息可能与哪个文件相关?

答: f.exe ? 生成失败算相关吗?错误信息中提至的 C0S 算吗?

在学习汇编语言的过程中使用link.exe将tc.exe生成的对象文件f.obj进行连接,并生成f.exe文件。通过调试器加载f.exe文件后深入分析并思考相关问题。

分析:
连接成功但有警告:LINK warning L4038:program has no starting address

1. f.exe 的程序代码总共有多少字节?

执行文件共有 541 个字节。
原始代码的实际长度为 29 个字节。
减去创建栈帧所需的操作(即 $push\ bp,\ $mov\ bp\ sp$) 共 4 个字节。
减去还原建栈帧所需操作(即 $pop\ bp$) 的 1 个字节。
子程序 $f()$ 最终占用 25 个字节。

在这里插入图片描述
在这里插入图片描述

2. f.exe 的程序能正确返回吗?

答: 执行后卡死,无法返回。

在这里插入图片描述

3. f 函数的偏移地址是多少?

*答:

3. 写一个程序 m.c

复制代码
    main()
    {
    	*(char far *)(0xb8000000+160*10+80)='a';
    	*(char far *)(0xb8000000+160*10+81)=2;
    }

通过 tc.exe 对 m.c 实现编译,并完成连接以生成 m.exe 文件;然后调用 Debug 工具分析整个程序的汇编代码,并进一步思考相关问题。

1. m.exe 的程序代码总共有多少字节?

答:25 字节。与 f 相比少了创建和还原栈帧的4字节

在这里插入图片描述

2. m.exe 能正确返回吗?

答: 正常返回

在这里插入图片描述

3. m.exe 程序中的 main 函数和 f.exe 中的f函数的汇编代码有何不同?

答: 除了_main和_f标号名称不同之外,在_f函数中多加了一条jmp short @1这条指令。然而从逻辑分析来看似乎没有实质性的效果。

在这里插入图片描述

4. 用 Debug 对 m.exe 进行跟踪:

① 找到对 main 函数进行调用的指令的地址;

在这里插入图片描述

答: 第一步定位到主函数的ret指令位置;接着使用t跟踪功能;然后返回至主函数调用记录中的具体地址位置;并使用u键移动至第110行,并往回滚动查看相关代码上下文;最终定位到了call标记位于第01FA地址。

特别提示

特别提示

复制代码
    076C:011E CALL  0214
    076C:0217 JMP 0223
    076C:022E CALL [0194]
    076C:0213  RET
    076C:0232 CALL [0196]
    076C:0213  RET
    076C:0236 CALL [0198]
    076C:0213  RET
    076C:023A PUSH [BP + 04]
    076C:023D CALL 0121
    076C:0126 CALL 01A5
在这里插入图片描述

076C:01AD INT 21 这里有个中止进制,但程序并没有退出来,继续…

在这里插入图片描述

最终在 076C:0156 INT 214Ch带返回码方式的终止进程)这里整个程序才完全返回。

5. 思考如下几个问题

1. 对 main 函数调用的指令和程序返回的指令是哪里来的?

答:c0s.obj

当没有主函数时,在错误信息中可以看到与cos相关的提示;而在此之前,在搭建开发环境时缺少了cos.obj文件以及tc.exe这个程序就无法对程序进行连接。那么tc.exe是否是将cos.obj文件与用户程序的.obj文件一起进行连接从而生成.exe文件呢?

答:

您想询问的是:执行由用户的程序在其主函数中发出的指令以及该程序返回的操作指令是否源自于c0s.obj文件?

答:

4. 我们如何看到 c0s.obj 文件中的程序代码呢?

请参考第 6 条(通过 link.exe 将 c:\TC20\LIB 目录下的 c0s.obj 连接生成文件 c0s.exe。) 然后 debug c0s.exe

5. c0s.obj 文件里有我们设想的代码吗?

答:

6. 用 link.exe 对 c:\minic(我的在 c:\TC20\LIB) 目录下的 c0s.obj 进行连接,生成 c0s.exe。

通过调试工具 Debug 分别查看 c0s.exem.exe 的源代码。特别提示:请从头开始查看这两个程序文件在源代码中存在哪些相似之处?

在这里插入图片描述

补充说明一下,我连接时报错了,但还是生成了 exe

在这里插入图片描述

通过调试器定位到 m.exe 中调用 main 函数的具体 call 指令位置,并记录其偏移量;随后查看该位置之后连续的 10 条指令内容;接着利用同样的方法加载 c0s.exe 应用程序,并记录相同偏移量处之后连续 10 条指令信息;最后对比两者的指令序列以寻找潜在关联

在这里插入图片描述

Turbo C编译器相关的整合模块负责将c0s.obj与用户自定义的.obj文件进行整合并生成.exe文件的过程及其内部工作原理。

连接过程

通过整合 c0s.obj(包含系统提供的初始化代码)与用户的 .obj 文件(包含用户编写的程序代码),tc.exe 实现了对两个对象文件的连接,并最终生成可执行的 .exe 文件。

程序运行流程

  • 初始化阶段 :按照c0s.obj加载初始化程序的主要作用是:获取所需的基础系统资源,并通过配置数据段寄存器DS和堆栈段寄存器SS等手段来为后续的操作做好必要准备。

  • 系统启动用户程序:系统通过调用用户代码中的 main 函数来完成启动过程。

  • 用户程序执行:在主函数中执行其逻辑功能。

  • 用户程序退出:当主函数返回时(通常是 0),将执行权限交给 c0s.obj 中的后续代码。

  • 清理与退出:当 c0s.obj 中的资源释放并环境恢复完成后(如堆栈空间归位),最终通过操作系统中断功能号 int 21h 的 4ch 功能号来实现正常的退出流程。

C程序从main 函数开始的保障机制

  • 系统功能:C开发环境(例如 Turbo C)提供了核心的初始化与终止功能模块,并将这些功能模块存储于诸如 c0s.obj 这类系统对象文件中。

    • 链接条件:用户编写的.obj文件必须与包含系统支持代码的.obj文件(如 c0s.obj)一同通过链接机制实现整合成完整的可执行程序。
    • 主流程管理:位于c0s.obj中的代码将在系统初始化完成后自动管理并执行用户编写的主函数代码。

灵活启动点

  • 修改启动行为 :从理论上讲,在文件c0s.obj中进行代码修改时,默认情况下会使其以调用其他函数的方式调用用户的程序而非以调用main函数的方式调用用户的程序。这样处理后即可使用户能够通过不使用main函数作为入口点的方式编写C程序。

该编译器通过将包含系统初始化和退出处理代码的c0s.obj文件与用户编写的目标文件连接起来,在符合标准C语言规范(即从main函数开始执行)的基础上生成可执行程序。这种机制保证了C程序能够正确地进行初始化操作、有效地管理资源以及与其他操作系统进行交互。此外,在特定情况下可以通过更改c0s.obj文件来定义程序启动时的行为,并非仅仅依赖于从main函数开始执行这一限制。

  • 程序 c0s.asm
复制代码
    assume cs:code
    data segment
    	db 128 dup (0)
    data ends
    
    code segment
     start:	mov ax,data
    		mov ds,ax
    		mov ss,ax
    		mov sp,128
    		
    		call s
    		
    		mov ax,4c00h
    		int 21h
    s: 	
    ; 编译连接后 f.c 对应的汇编代码就直接拼在这后面。
    ; 所以上面 call s 后就是调用 f() 了
    
    code ends
    end start

9. 用 tc.exe 将 f.c 重新进行编译,连接,生成 f.exe。

这次能通过连接吗?
答:

在这里插入图片描述

f.exe 可以正确运行吗?
答: 可以

在这里插入图片描述

Debug 察看 f.exe 的汇编代码。
答: 给你:

在这里插入图片描述

基于新的c0s.obj构建基础框架后,请生成一个新的f.c文件,并对其中的安全性进行全面分析与深入理解。

复制代码
    #define Buffer ((char *)*(int far *)0x200) 
    
    f()
    {
    	Buffer=0;
    	Buffer[10]=0;
    	
    	while(Buffer[10]!=8){
    		Buffer[Buffer[10]]='a'+Buffer[10];
    		Buffer[10]++;
    	}
    }

debug 查看反汇编:

复制代码
    ; ======================== c0s.obj ========================
    076C:00000000 B8060E              mov  ax,076C                                  
    076C:00000003 8ED8                mov  ds,ax                                    
    076C:00000005 8ED0                mov  ss,ax                                    
    076C:00000007 BC8000              mov  sp,0080           
                       
    076C:0000000A E80500              call 00000012 ($+5)     
                      
    076C:0000000D B8004C              mov  ax,4C00                                  
    076C:00000010 CD21                int  21                   
                    
    ; ========================== f.c ==========================
    ; ---------------------------------------------------------
    ; 对应:Buffer=0;
    ; ---------------------------------------------------------
    076C:00000012 33DB                xor  bx,bx                                    
    076C:00000014 8EC3                mov  es,bx                                    
    076C:00000016 BB0002              mov  bx,0200                                  
    076C:00000019 26C7070000          mov  word es:[bx],0000      es:[0200]=0000 
      
    ; ---------------------------------------------------------
    ; 对应:Buffer[10]=0;
    ; ---------------------------------------------------------
    076C:0000001E 33DB                xor  bx,bx                                    
    076C:00000020 8EC3                mov  es,bx                                    
    076C:00000022 BB0002              mov  bx,0200                                  
    076C:00000025 268B1F              mov  bx,es:[bx]             es:[0200]=0000    
    076C:00000028 C6470A00            mov  byte [bx+0A],00        ds:[000A]=0000  
    
    ; ---------------------------------------------------------
    ; 对应:while		(循环条件在下面 076C:0000006A)
    ; ---------------------------------------------------------
    076C:0000002C EB3C                jmp  short 0000006A ($+3c)        
    ; ---------------------------------------------------------
    ; 对应:{			(while 循环体从这开始)
    ; ---------------------------------------------------------
    076C:0000002E 33DB                xor  bx,bx                                    
    076C:00000030 8EC3                mov  es,bx                                    
    076C:00000032 BB0002              mov  bx,0200                                
    076C:00000035 268B1F              mov  bx,es:[bx]             es:[0200]=0000
    076C:00000038 8A470A              mov  al,[bx+0A]             ds:[000A]=00    	; al=Buffer[10]
    076C:0000003B 0461                add  al,61                                    ; al='a'+al
    
    076C:0000003D 33DB                xor  bx,bx                                    
    076C:0000003F 8EC3                mov  es,bx                                    
    076C:00000041 BB0002              mov  bx,0200                                  
    076C:00000044 268B1F              mov  bx,es:[bx]             es:[0200]=0000   
    076C:00000047 50                  push ax                                       
    076C:00000048 53                  push bx                                       
    076C:00000049 33DB                xor  bx,bx                                   
    076C:0000004B 8EC3                mov  es,bx                                    
    076C:0000004D BB0002              mov  bx,0200                                  
    076C:00000050 268B1F              mov  bx,es:[bx]             es:[0200]=0000   
    076C:00000053 8A470A              mov  al,[bx+0A]             ds:[000A]=00
    076C:00000056 98                  cbw                                           
    076C:00000057 5B                  pop  bx                                       
    076C:00000058 03D8                add  bx,ax                                    
    076C:0000005A 58                  pop  ax                                       
    076C:0000005B 8807                mov  [bx],al                ds:[0000]=00		; 存入字符
    ; ---------------------------------------------------------
    ; 对应:Buffer[10]++;
    ; ---------------------------------------------------------
    076C:0000005D 33DB                xor  bx,bx                                    
    076C:0000005D 33DB                xor  bx,bx                                    
    076C:0000005F 8EC3                mov  es,bx                                    
    076C:00000061 BB0002              mov  bx,0200                                  
    076C:00000064 268B1F              mov  bx,es:[bx]             es:[0200]=0000
    076C:00000067 FE470A              inc  byte [bx+0A]           ds:[000A]=1D9F    ; 索引++
    
    ; ---------------------------------------------------------
    ; 对应:while 的循环条件 Buffer[10]!=8
    ; ---------------------------------------------------------
    076C:0000006A 33DB                xor  bx,bx                                    
    076C:0000006C 8EC3                mov  es,bx                                    
    076C:0000006E BB0002              mov  bx,0200                                  
    076C:00000071 268B1F              mov  bx,es:[bx]             es:[0200]=0000
    076C:00000074 807F0A08            cmp  byte [bx+0A],08        ds:[000A]=0000
    076C:00000078 75B4                jne  0000002E ($-4c)        
    ; ---------------------------------------------------------
    ; 对应:}
    ; ---------------------------------------------------------
    
    076C:0000007A C3                  ret                                   
在这里插入图片描述

研究试验 5 函数如何接收不定数量的参数

1. 分析程序 a.c

通过运行 tc.exe 对 a.c 进行编译、链接操作以生成 a.exe;通过调用 Debug 工具加载 a.exe 并对其函数汇编代码进行详细分析;分别解答以下两个问题:main 函数是如何给 showchar 传递参数的?showchar 是如何接收参数的?

复制代码
    void showchar(char a, int b);
    
    main()
    {
    	showchar('a',2);
    }
    
    void showchar(char a, int b)
    {
    	*(char far *)(0xb8000000+160*10+80)=a;
    	*(char far *)(0xb8000000+160*10+81)=b;
    }
在这里插入图片描述

main 将参数依次入栈供调用函数使用,并将结果作为返回值返回给主程序函数体调用入口点。在子函数体执行过程中,在适当的位置按照偏移量从栈顶弹出并获取两个操作数进行处理后即可完成任务并返回到父函数体继续执行后续指令。

给函数传参时参数传递顺序为从右向左。
当函数被调用时系统会将当前线程的基址(BP)压入栈中对应 [bp+0] 的位置。
执行 call 指令后程序计数器(IP)也会被压入栈以便返回时能正确复位 并对应 [bp+2] 的位置。

在这里插入图片描述
复制代码
    ; ========================== main ==========================
    	mov	ax,2							; 首先最右边的参数 2 入栈
    	push	ax
    	mov	al,97							; 其次参数 'a' 入栈
    	push	ax
    	call	near ptr _showchar			; 调用函数 showchar(会将下一句的 IP 入栈)
    	pop	cx
    	pop	cx
    
    	ret	
    ; ======================== showchar ========================
    	push	bp							; 保护调用方栈帧基址
    	mov	bp,sp							; 在栈顶创建新栈帧基址
    
    	mov	al,byte ptr [bp+4]				; 从栈中取出 'a'
    	mov	bx,0B800h						; 写入内存 B800:0690
    	mov	es,bx
    	mov	bx,0690
    	mov	byte ptr es:[bx],al
    
    	mov	al,byte ptr [bp+6]				; 从栈中取出 2
    	mov	bx,0B800h						; 写入内存 B800:0691
    	mov	es,bx
    	mov	bx,0691
    	mov	byte ptr es:[bx],al
    
    	pop	bp
    	ret	

2. 分析程序 b.c

复制代码
    void showchar(int,int,...);
    
    main()
    {
    	showchar(8,2,'a','b','c','d','e','f','g','h');
    }
    
    void showchar(int n, int color, ...)
    {
    	int a;
    	for(a=0; a!=n; a++)
    	{
    		*(char far *)(0xb8000000+160*10+80+a+a)=*(int *)(_BP + 8 + a + a);
    		*(char far *)(0xb8000000+160*10+81+a+a)=color;
    	}
    }
在这里插入图片描述

深入理解相关的知识。思考:

How does the showchar function determine how many characters to display?
Answer: The first argument passed to showchar specifies the number of characters.

printf 函数是如何知道有多少个参数的?
答: 调用 printf 对应 CALL 0AC1

在这里插入图片描述

1. 参数为单一字符的字符串直接通过ax加载到 printf 中。
2. 当存在多个字符串时,则将这些字符串依次压入栈中传递给 printf。
3. 当使用 printf("%s %s\n", "Hello", "World") 这样的语法时,则会将三个参数依次被压入栈中传递给 printf。
3.1. print函数根据第一个参数中的格式说明符%的数量来确定要处理的参数数量。
3.2. print函数在处理第一个参数时会逐个检查其中的%符号,在遇到每个%符号后就取出相应的后续参数进行处理。

3. 实现一个简单的 printf 函数,只需要支持 %c%d 即可。

  1. 使用masm工具生成可执行文件myprintf.obj。
  2. 使用编译器tcc将源文件main.c转换为main.obj。
  3. 使用链接器将c0s.obj、main.obj、myprintf.obj以及cs.lib链接并生成主程序main.exe。
  • 编译,连接辅助bat脚本(我的 tc2.0 在c:\tc20
复制代码
    cls
    del main.exe
    del main.map
    del *.obj
    masm myprintf.asm;
    tcc -c main.c
    tlink c:\tc20\lib\c0s.obj main.obj myprintf.obj, main.exe,,c:\tc20\lib\cs.lib

myprintf.asm

用汇编实现一个 myprintf 方法,供 turbo c 调用。

复制代码
    ; 定义常量:缓冲区大小
    BUFF_LEN EQU 50h
    
    ; 定义代码段
    _TEXT	segment	byte public 'CODE'
    	assume	cs:_TEXT
    
    ; ================ 实现 _myprintf 方法供 TC 调用 ===============
    ; 这里叫 _myprintf 在 TC 中调用时不需要下划线,直接写 myprintf
    ; --------------------------------------------------------------
    public _myprintf	; 声明为外部可用
    _myprintf proc
    		push bp
    		mov bp,sp
    		sub sp,128		; 开辟 128 的局部空间(SP 到 BP 之间这一段)
    		push si
    		push di
    		
    		; [bp-128]存放标志【-】:左对齐。若指定宽度,数据会在指定宽度内左对齐,右侧填充值。
    		; [bp-127]存放标志【+】:始终显示正负号。+1, -1, +0
    		; 				  【空格】:正数前面输出空格,负数前仍然输出 -
    		; [bp-126]:未使用
    		; [bp-125]存放标志【0】:用 0 填充宽度。适用于数值输出,且通常与宽度修饰符一起使用。
    		; [bp-124]存修饰符【*】:参数指定宽度,类型 int 。printf("%*c", 5, 'A');
    		; [bp-123]~[bp-122]存:显示宽度 初始化为0
    		mov word ptr [bp-123],0
    		
    		; [bp-100]存放缓冲区长度,默认 BUFF_LEN,满了就打印一次
    		mov [bp-100],BUFF_LEN
    		; [bp-102] 存缓冲区长度的地址
    		lea di,[bp-100]		; 拿出地址
    		mov [bp-102],di		; 存进去
    		
    		lea di,[bp-99]				; di 指向缓冲区首字地址
    
    		; [bp-4]~[bp-3]存: 格式说明字符串遍历到第几个字符(字符的偏移量)
    		mov si,[bp+4]				; 默认从参数1开始开头开始,si 取参数1地址
    		mov [bp-4],si				; 
    		mov [bp-8],di				; 存缓冲区首字地址
    		
    		; [bp-6]~[bp-5]缓存:待处理的参数地址(初始是第2个然后是3、4、5...)
    		lea ax,[bp+6]
    		mov [bp-6],ax
    		
    		; -------------- 遍历格式说明字符串 --------------
    loopp1: 
    		mov [bp-4],si			; 
    		mov [bp-8],di			; 更新待处理的参数地址	
    		; 从参数1字符串中,读取一个字符到 al
    		; 如果取到 0 表示格式说明字符串结束跳 ctrl_string_end
    		; 如果取到格式说明符 % 调用 ctrl_string 解析格式说明符
    		; 如果取到普通字符,存入缓冲区
    		lodsb				; 串操作从 ds:[si] 取一个字节,然后 si++
    			
    		; 每次开始解析格式说明符前都清一下标志位
    		mov byte ptr [bp-128],0		; 80
    		mov byte ptr [bp-127],0     ; 7F
    		mov byte ptr [bp-125],0     ; 7D
    		mov byte ptr [bp-124],0     ; 7C
    		mov word ptr [bp-123],0     ; 7B
    		
    		or al,al
    		je ctrl_string_end
    		
    		cmp al,'%'
    		je ctrl_string
    		
    normal_char:	; 普通字符
    		push [bp-102]		; 缓冲区长度地址
    		call append_char_to_buffer
    		jmp loopp1			; 继续遍历下一个格式说明符
    		
    ctrl_string:	; 格式说明符
    		call parse_ctrl_string
    		jmp loopp1
    
    ; 格式说明字符串结束
    ctrl_string_end:
    		push [bp-102]		; 缓冲区长度地址
    		call print_buffer
    err:
    		pop di
    		pop si
    		mov sp,bp			; 释放局部空间
    		pop bp				; 还原栈帧基址为调用者
    		ret
    _myprintf endp
    ; ================ 实现 _myprintf 方法供 TC 调用 ===============
    
    ; ====================== 向缓冲区追加字符 ======================
    ; 参数:di		ds:[di] 指向追加的位置
    ; 参数:al		要追加的字符
    ; 参数:[bp+4]		缓冲区长度地址
    ; --------------------------------------------------------------
    append_char_to_buffer proc
    	push bp
    	mov bp,sp
    	
    		mov bx,[bp+4]		; 缓冲区长度地址
    
    		mov [di],al			; 字符存入缓冲区
    		inc di				; 指向缓冲区中下一个字符位置
    		
    		mov bx,[bp+4]	
    		; 检测缓冲区长度如果 >0 表示没满,继续遍历下一个格式说明符
    		dec byte ptr [bx]				; 长度 - 1,
    		jg append_char_to_buffer_end	; 如果 >0 表示未满,本次追加结束
    		push bx
    		call print_buffer					; 否则已满,调用 print_buffer 清空弹夹
    		
    append_char_to_buffer_end:
    	mov sp,bp
    	pop bp
    	ret 2
    append_char_to_buffer endp
    ; ====================== 向缓冲区追加字符 ======================
    
    ; ========================== 填充字符 ==========================
    ; 参数:dl		要追加的字符
    ; 参数:di		ds:[di] 指向追加的位置
    ; 参数:cx		填充个数
    ; --------------------------------------------------------------
    pad_char proc
    	cmp cx,0
    	jbe pad_char_end
    pad_char_start:
    	mov [di],dl
    	inc di
    	loop pad_char_start
    pad_char_end:
    	ret
    pad_char endp
    ; ========================== 填充字符 ==========================
    
    ; ====================== 输出并清空缓冲区 ======================
    ; 参数:di			缓冲区当前位置
    ; 参数:[bp+4]		缓冲区长度地址
    ; 					缓冲区的首地址 = 长度地址 + 1
    ; --------------------------------------------------------------
    print_buffer proc
    		push bp
    		mov bp,sp
    		push ax
    		push bx
    		push dx
    		
    		mov [di],'$'	; 按 09h 需求,在后面加上结束符
    		mov dx,[bp+4]	; 缓冲区长度地址
    		;mov bx,dx
    		inc dx			; 缓冲区的首地址
    		;add dx,2		
    		mov ah,09h		; 在Teletype模式下显示字符串
    		int 21h
    		
    		mov word ptr [bp+4],BUFF_LEN	; 重置缓冲区长度
    		mov di,dx						; 重置di指向缓冲区起始位置
    		
    		pop dx
    		pop bx
    		pop ax
    		mov sp,bp
    		pop bp
    		ret 2
    print_buffer endp
    ; ====================== 输出并清空缓冲区 ======================
    
    ; ======================= 解析格式说明符 =======================
    ; 遇到 % 的处理逻辑。将解析出来的标志、打印宽度,存到 [bp-128] 开始的一系列内存单元中
    ; 最终于到 c 则取出待处理参数,然后调用 do_show_char 打印字符
    ; 最终于到 d 则取出待处理参数,然后调用 do_show_digit 打印整数
    ; 结果到结束都没遇到 c, d 则直接按普通字符显示
    ; --------------------------------------------------------------
    ; 参数:ds:si	指向要解析的目标字符串
    ; 返回:ax		返回数值
    ; --------------------------------------------------------------
    parse_ctrl_string proc
    	push bx
    	push cx
    	
    parse_ctrl_string_start:
    		lodsb
    		
    		cmp al,'%'		; %=25 %后面的 % 作为普通字符直接显示
    		je normal_percent_sign
    
    ; 修饰项(用于说明宽度、小数位、填充情况)
    		; 标志判断
    		cmp al,'-'		; -=2D 左对齐
    		je left_justify		
    		cmp al,'+'		; +=2B 【+】:始终显示正负号。+1, -1, +0
    		je always_signs
    		cmp al,' '		;  =20【 】:只显示负号(正数前面输出空格)
    		je only_negative
    		cmp al,'0'		; 0=30【0】:用 0 填充宽度(只对数字生效)
    		je zero_padding
    		cmp al,'*'		; *=2A 传参指定宽度
    		je set_print_span
    		
    		cmp al,'0'		; 0=30 格式说明字符串中指定的宽度
    		jb numend		; 小于 0 非数字,跳 numend
    		cmp al,'9'		; 9=39 格式说明字符串中指定的宽度
    		jg numend		; 大于 9 非数字,跳 numend
    		jmp parset_print_span	; 否则将数字解析为打印宽度
    numend:
    
    ; 格式说明符
    format_start:
    		cmp al,'c'		; c=63 显示字符
    		je show_char		
    		cmp al,'d'		; d=64 显示整数
    		je show_digit
    		
    		or al,al					; 读到格式说明字符串结束
    		je parse_ctrl_string_end		; 跳返回
    		
    ; 不是任何格式说明符,则从上一次遇到的 % 开始到字符串结尾,全当普通字符(通畅是格式说明符写错了)
    pcs_normal_char:
    		; 恢复到上一个 % 时的状态:si,[bp-102], di
    		mov si,[bp-4]				; 从上一次遇到的 % 的位置
    		mov al,[si]
    		inc si
    		
    		sub di,[bp-8]				; 当前位置 - %位置 = 要回退的长度
    		sub [bp-102],di				; 原长度 - 要回退的长度 = 新长度
    		
    		mov di,[bp-8]				; 原%位置放回 di
    	pcs_normal_char_s:
    		push [bp-102]				; 缓冲区长度地址
    		call append_char_to_buffer
    		lodsb
    		or al,al					; 没读到字符串结束
    		jne pcs_normal_char_s			; 就一直继续
    		dec si							; 为了返回上一层也能正常退出,后退一位,让上一层也读到 0 就能正常退出了
    		jmp parse_ctrl_string_end	; 全部作为普通字符输出完成后,跳结束
    
    normal_percent_sign:	; 普通字符 %
    		push [bp-102]		; 缓冲区长度地址
    		call append_char_to_buffer
    		jmp parse_ctrl_string_end			; 本次%已经消费,跳结束
    
    ; 标志只需要先存下来
    left_justify:			; 左对齐:保存标志,继续遍历下一个格式说明符
    		mov [bp-128],al													
    		jmp parse_ctrl_string_start                                     
    always_signs:			; 【+】:始终显示正负号。+1, -1, +0              
    		mov [bp-127],al                                                 
    		jmp parse_ctrl_string_start                                     
    only_negative:			; 【 】:只显示负号(正数前面输出空格)          
    		mov [bp-127],al                                                 
    		jmp parse_ctrl_string_start                                     
    zero_padding:			; 【0】:用 0 填充宽度(只对数字生效)           
    		mov [bp-125],al
    		jmp parse_ctrl_string_start                                     
    parset_print_span:		; 解析打印宽度                                  
    		call _parse_int                                                 
    		jmp parse_ctrl_string_start                                     
    set_print_span: 		; * 参数指定宽度                                
    		mov [bp-124],al	                                                
    		
    		; 既然传了宽度参数,那就取出来放到[bp-123]待用
    		mov bx,[bp-6]			; 取出宽度参数地址
    		mov cx,[bx]				; 取出宽度参数
    		mov word ptr [bp-123],cx; 存到宽度位置
    			
    		add [bp-6],2			; 指向下一个待处理参数
    		
    		lodsb					; * 号后只认格式说明符,取一个字符后
    		jmp format_start			; 跳 format_start 继续判断
    ; 格式说明符,按要求处理
    show_char:				; 字符格式
    		call do_show_char
    		jmp parse_ctrl_string_end
    show_digit:				; 整数格式
    		call do_show_digit
    		
    parse_ctrl_string_end:
    	pop cx
    	pop bx
    	ret
    parse_ctrl_string endp
    ; ======================= 解析格式说明符 =======================
    
    ; ======================= 打印格式为字符 =======================
    ; 按先前解析的格式打印字符
    ; 此处没有创建新栈帧,继续沿用了调用者的,便于直接取之前解析的格式说明和修饰
    ; --------------------------------------------------------------
    ; 参数:al			待打印的字符
    ; 参数:ds:[di]		缓冲区当前可用位置的地址
    ; --------------------------------------------------------------
    do_show_char proc
    	push bx
    	push cx
    	push dx
    		
    	mov cx,[bp-123]			; 从统计结果中取出宽度
    	cmp cx,0
    	jbe align_o				; 小等于 0 说明没指定
    	dec cx					; 否则:宽度 - 1字符 = 填充长度
    
    align_o:
    	mov bx,[bp-6]			; 取出字符(对于 c 存的是字符值)
    	mov al,[bx]
    	mov dl,' '				; 设置填充字符
    	
    	cmp [bp-128],'-'		; 判断对齐方向 '-' = 2D
    	jne align_righ
    	jmp align_left
    
    align_righ:
    	call pad_char			  	; 填充空格
    	push [bp-102]				; 缓冲区长度地址
    	call append_char_to_buffer	; 字符追加写入缓冲区
    	jmp do_show_char_end
    
    align_left:
    	push [bp-102]				; 缓冲区长度地址
    	call append_char_to_buffer	; 字符追加写入缓冲区
    	call pad_char			  	; 填充空格
    
    do_show_char_end:
    	mov word ptr [bp-123],0		; 宽度清零
    	
    	
    	add [bp-6],2			; 指向下一个待处理参数
    		
    	pop dx
    	pop cx
    	pop bx
    	ret
    do_show_char endp
    ; ======================= 打印格式为字符 =======================
    
    ; ======================= 打印格式为整数 =======================
    ; 按先前解析的格式打印整数
    ; 此处没有创建新栈帧,继续沿用了调用者的,便于直接取之前解析的格式说明和修饰
    ; --------------------------------------------------------------
    ; 参数:ds:[di] 	结果字符串地址
    ; --------------------------------------------------------------
    do_show_digit proc
    	pop dx
    	push bx
    	
    	xor bx,bx
    	; 当前 di 已指向结果字符串地址
    	; 调用子程序 dtoc 将 word 转 10 进制数字
    	mov bl,[bp-128]			; -左对齐,否则右对齐
    	push bx
    	mov bl,[bp-127]			; 符号规则
    	push bx
    	mov bl,[bp-123]			; 宽度
    	push bx
    	mov bl,[bp-125]			; 填充字符
    	push bx
    	mov bx,[bp-6]			; 16位有符号整数(先取地址)
    	push [bx]					; 再取值
    	call dtoc
    	
    	push [bp-102]			; 缓冲区长度地址		
    	call print_buffer		; 清空缓冲区
    	mov word ptr [bp-123],0	; 宽度清零
    	add [bp-6],2			; 指向下一个待处理参数
    	
    	pop bx
    	push dx
    	ret
    do_show_digit endp
    ; ======================= 打印格式为整数 =======================
    
    ; ======================== 字符串转整数 ========================
    ; 将字符串形式的数字解析为数值 
    ; 从左向右逐个读取字符,只要是数字就处理,最终返回对应的数值
    ; --------------------------------------------------------------
    ; 参数:ds:si	指向要解析的目标字符串
    ; 返回:ax		返回数值
    ; --------------------------------------------------------------
    _parse_int proc
    	cbw
    _parse_int_start:
    	sub al,'0'					; ascii 数字转数值
    	
    	; 每向后取到一个数字,之前的累加结果就要进位(x10)再和本次的相加
    	xchg [bp-0123],ax 			; 当前值与累加值互换
    	; 累加值 = 累加值 x 10 (位运算模拟x10:拆分成 x2 + x8)
    	shl  ax,1
    	mov  dx,ax
    	shl  ax,1
    	shl  ax,1
    	add  ax,dx
    	; 累加值 = 累加值 + 当前值
    	add  [bp-0123],ax
    	
    	; 累加完成再读下一个
    	lodsb
    	
    	; 如果不是数字就结束,否则继续
    	cmp al,'0'		
    	jb _parse_int_end		
    	cmp al,'9'		
    	jg _parse_int_end		
    	jmp _parse_int_start
    	
    _parse_int_end:
    	dec si						; 当前不是数字退出的,si 后退一位一遍上层逻辑继续
    	mov ax,[bp-0123]			; 返回 %所标示的打印宽度
    	ret
    _parse_int endp
    ; ======================== 字符串转整数 ========================
    
    ; ======================== 整数转字符串 ========================
    ; 整数转字符串,以 0 结尾
    ; 1. 先判断要处理的整数正负,并做好标记(负数要转为原码,才能下一步)
    ; 2. 用除10取余的方式将整数转为字节
    ; 2.1. 因为这样得到的结果是倒序的比如 12345 得到结果的顺序是 54321
    ;	   所以我们将每次得到的余数转字符压栈,后续再出栈到结果字符位置就行了。
    ; 3. 计算填充长度
    ; 3.1. 如果字符串长度 > 指定结果字符串长度,不用填充,否则进行填充
    ; 4. 填充
    ;	4.1. 根据符号规则与对齐方式有4种填充方式,分别是:
    ;   	【无填充、左对齐填充、右对齐填充空格、右对齐填充0】
    ; 5. 最后统计一下字符串长度,存到 ax 中作为返回值
    ; --------------------------------------------------------------
    ; 参数:[bp+4]	16位有符号整数
    ; 参数:[bp+6]	填充字符
    ; 参数:[bp+8]	指定结果字符串长度
    ; 参数:[bp+10]	符号规则【 +  】:始终显示+-号
    ;						【空格】:正数显示空格,负数显示-
    ; 参数:[bp+12]	左对齐	【 -  】:左对齐,否则右对齐
    ; 参数:ds:[di] 结果字符串地址
    ; --------------------------------------------------------------
    ; 返回: ax		字符串长度
    ; --------------------------------------------------------------
    dtoc proc
    		push bp
    		mov bp,sp
    		sub sp,16
    		push bx
    		push cx
    		push dx
    		push es
    		push si
    
    		mov [bp-2],di				; 保存字符串开始位置
    		jmp dtoc_start				; 跳到主逻辑代码开始位置
    
    ; ----- 处理符号 -----
    sign_start:
    		cmp byte ptr [bp-3],'-'		; 判断当前数的正负(之前算过标记在这的)
    		je negative_number
    positive_number:
    	dtoc_always_signs:
    		cmp word ptr [bp+10],'+'				; 始终显示+-号
    		jne dtoc_only_negative
    		mov byte ptr [di],'+'
    		jmp sign_end
    	dtoc_only_negative:	
    		cmp byte ptr [bp+10],' '				; 正数显示空格,负数显示-
    		jne sign_ret							; 不是 + 也不是空格则是默认规则:正数不显示,跳返回
    		mov byte ptr [di],' '					; 否则正数显示空格
    		jmp sign_end
    negative_number:
    		mov byte ptr [di],'-'				; 加上负号
    sign_end:
    		inc di						; 符号占一位,di 后移
    		
    sign_ret:
    		ret
    ; ----- 填充 -----
    	pad_start:
    		xchg [bp+8],cx
    		mov ax,[bp+6]	; 取填充字符(只用 al)
    		cld				; 设置 DF = 0,,串操作时方向 ++
    		push ds
    		pop es
    		rep stosb		; 重复 cx 次,ds:[di] = al 然后 di++
    		xchg [bp+8],cx	; 取回结果字符长度
    		
    		ret
    ; ----- 取出字符 -----
    ; 目前栈里是 54321 顺序不是我们想要的,遍历一下出栈到 ds:[di]... 就正了
    	pop_char:
    		pop dx
    	pop_char_s:
    		pop ax
    		mov [di],al	; 写入目标内存地址。第 di 个字符(我们只取低8位有效值)
    		inc di
    		loop pop_char_s
    		push dx
    		ret
    
    ; --------------------------------------------------------------
    dtoc_start:
    ; ----- 判断正负 -----
    		mov byte ptr [bp-3],'+'		; 先假设是正数,标记符号为 + 
    		mov ax,[bp+4]
    		mov bx,ax
    		and bx,1000000000000000b
    		cmp bx,1000000000000000b	; 符号位==1是负数
    		jne convert					; 如果是正数直接返回
    			dec ax						; 补码转反码
    			not ax						; 反码转原码
    			mov byte ptr [bp-3],'-'		; 标记符号为 -
    
    ; ----- 执行转换 -----
    convert:
    		; 数字转字符(用除10取余实现)
    		xor cx,cx			; 循环变量从 0 开始
    
    		; 用遍历取余的方式拿到 ascii 拿到的结果是倒的
    		; 比如 12345 遍历取余拿到的是 54321 所以先丢栈里,方便下一步翻转
    		mov bx,10
    dtoc_s:	xor dx,dx	; 除数 bx 是 16 位,则被除数是32位。高16位 dx 没有数据要补 0
    		div bx		; 除完后 商在ax下次继续用,dx为余数
    		add dx,30H	; 将余数转 ascii 
    		push dx		; 入栈备用(此时高8位是无意义的,之后我们只要取低8位用即可 )
    		inc cx		; 循环变量 +1
    		cmp ax,0	; 如果商为0跳出循环
    		je dtoc_s_ok
    		jmp dtoc_s			; 否则继续循环
    dtoc_s_ok:
    
    ; ----- 填充长度 -----
    ; 计算
    count_len:
    		cmp word ptr [bp+8],0		; 如果指定宽度 <=0 设为 0
    		jge count_pad_len
    		mov word ptr [bp+8],0
    	count_pad_len:
    		cmp [bp+8],cx
    		jbe no_pad					; 指定长度 < 字符串长度 :直接跳过,不用填充
    		sub [bp+8],cx				; 指定长度 - 字符串长度 = 填充个数
    		
    		; 如果符号为正,且符号显示规则为 0,则直接开始填充,否则填充长度 -1
    		cmp byte ptr [bp-3],'+'
    		jne pan_len_dec
    		cmp byte ptr [bp+10],0
    		je alr
    	pan_len_dec:
    		dec word ptr [bp+8]			; 宽度 -1
    		
    ; 填充开始
    	alr:
    		cmp byte ptr [bp+12],'-'	; 如果是右对齐
    		jne aright		
    	aleft:							; 左对齐逻辑
    		call sign_start
    		call pop_char
    		mov byte ptr [bp+6],' '		; 左对齐只能在右侧填充空格,填0值就不对了
    		call pad_start
    		jmp ret_l
    	aright:							; 右对齐逻辑
    		cmp byte ptr [bp+6],'0'		; 填充0:填在符号与数字之间 【+00012345】
    		je pad0
    			mov byte ptr [bp+6],' '	; 填充空格:填在符号之前    【   +12345】
    			call pad_start
    			call sign_start
    			call pop_char
    			jmp ret_l
    		pad0:
    			call sign_start
    			call pad_start
    			call pop_char
    		jmp ret_l
    	no_pad:							; 无需填充
    		call sign_start
    		call pop_char
    
    ; ----- 返回 -----
    ret_l:
    		mov ax,di			; 取字符串最后一个位置
    		sub ax,[bp-2]			; 尾 - 头 = 长度
    		mov byte ptr [di],0	; 字符串以 0 结束
    
    		pop si
    		pop es
    		pop dx
    		pop cx
    		pop bx
    		add sp,16			; 释放局部空间
    		mov sp,bp
    		pop bp
    		ret 10
    dtoc endp
    ; ======================== 整数转字符串 ========================
    
    _TEXT	ends
    end

main.c

测试 %c
复制代码
    extern void myprintf(const char *format, ...);
    
    void main() {
    	myprintf("================================================================================");
    	myprintf("myprintf		|1234567890|\n\r");
    	myprintf("--------------------------------------------------------------------------------");
    	myprintf("myprintf (%%0c):		|%0c|\n\r", 'a');
    	myprintf("myprintf (%%c):		|%c|\n\r", 'a');
    	myprintf("myprintf (%%5c):		|%5c|\n\r",'a');
    	myprintf("myprintf (%%10c):	|%10c|\n\r", 'a');
    	myprintf("myprintf (%%-10c):	|%-10c|\n\r", 'a');
    	myprintf("myprintf (%%*c):		|%*c|\n\r", 10,'a');
    	myprintf("myprintf (%%-*c):	|%-*c|\n\r", 10,'a');
    	myprintf("myprintf (%%c|%%5c|%%10c|%%-10c|%%*c):|%c|%5c|%10c|%-10c|%*c|\n\r", 'a', 'b', 'c', 'd', 10,'e');
    	myprintf("================================================================================");
    	myprintf("myprintf (%%-*-c):	|%-*-c|\n\r", 10,'a');
    	return;
    }
在这里插入图片描述
测试 %d 未指定宽度
复制代码
    extern void myprintf(const char *format, ...);
    
    void main() {
    	myprintf("================================================================================");
    	myprintf("myprintf		|1234567890|\n\r");
    	myprintf("--------------------------------------------------------------------------------");
    	myprintf("myprintf (%%d, 123):	|%d|\n\r", 123);
    	myprintf("myprintf (%%d, -123):	|%d|\n\r", -123);
    	myprintf("myprintf (%%+d, 123):	|%+d|\n\r", 123);
    	myprintf("myprintf (%%+d, -123):	|%+d|\n\r", -123);
    	myprintf("myprintf (%% d, 123):	|% d|\n\r", 123);
    	myprintf("myprintf (%% d, -123):	|% d|\n\r", -123);
    	myprintf("myprintf (%%0d, 123):	|%0d|\n\r", 123);
    	myprintf("myprintf (%%0d, -123):	|%0d|\n\r", -123);
    	myprintf("myprintf (%%0aed, -123):	|%0d|\n\r", -123);
    	return;
    }
在这里插入图片描述
测试 %d 指定宽度
复制代码
    extern void myprintf(const char *format, ...);
    
    void main() {
    	myprintf("================================================================================");
    	myprintf("myprintf		|1234567890|\n\r");
    	myprintf("--------------------------------------------------------------------------------");
    	myprintf("myprintf (%%10d, 123):	|%10d|\n\r", 123);
    	myprintf("myprintf (%%10d, -123):	|%10d|\n\r", -123);
    	myprintf("myprintf (%%010d, -123):	|%010d|\n\r", 123);
    	myprintf("myprintf (%%010d, -123):	|%010d|\n\r", -123);
    	myprintf("myprintf (%%-10d, 123):	|%-10d|\n\r", 123);
    	myprintf("myprintf (%%-10d, -123):	|%-10d|\n\r", -123);
    	myprintf("myprintf (%%-+10d, 123):	|%-+10d|\n\r", 123);
    	myprintf("myprintf (%%-+10d, -123):|%-+10d|\n\r", -123);
    	myprintf("myprintf (%%- 10d, 123):	|%- 10d|\n\r", 123);
    	myprintf("myprintf (%%- 10d, -123):|%- 10d|\n\r", -123);
    	return;
    }
在这里插入图片描述
测试 %d 单独用【参数】指定宽度
复制代码
    extern void myprintf(const char *format, ...);
    
    void main() {
    	myprintf("================================================================================");
    	myprintf("myprintf			|1234567890|\n\r");
    	myprintf("--------------------------------------------------------------------------------");
    	myprintf("myprintf (%%*d, 10, 123):	|%*d|\n\r", 10, 123);
    	myprintf("myprintf (%%*d, 10, -123):	|%*d|\n\r", 10, -123);
    	myprintf("myprintf (%%+*d, 10, 123):	|%+*d|\n\r", 10, 123);
    	myprintf("myprintf (%%+*d, 10, -123):	|%+*d|\n\r", 10, -123);
    	myprintf("myprintf (%% *d, 10, 123):	|% *d|\n\r", 10, 123);
    	myprintf("myprintf (%% *d, 10, -123):	|% *d|\n\r", 10, -123);
    	myprintf("myprintf (%%-+*d, 10, 123):	|%-+*d|\n\r", 10, 123);
    	myprintf("myprintf (%%-+*d, 10, -123):	|%-+*d|\n\r", 10, -123);
    	myprintf("myprintf (%%- *d, 10, 123):	|%- *d|\n\r", 10, 123);
    	myprintf("myprintf (%%- *d, 10, -123):	|%- *d|\n\r", 10, -123);
    	return;
    }
在这里插入图片描述

总结

反汇编分析 C

在反汇编分析时,请先执行$tcc -S demo.c命令以获取汇编文件asm。
随后通过运行masm命令:masmdemo.asm,demo.obj,demo.lst;来完成辅助分析。
当偏移量不匹配时,请在汇编文件asm中手动设置偏移量或指定起始位置。
例如,在C语言中可以通过以下代码片段打印main函数的入口地址:
printf("%x\n", main);

参考资料

stormpeach:《深入探讨tcc与tlink编译器连接器机制的研究》
Turbo_C_User_Guide_Ver_2.0_1988.pdf

全部评论 (0)

还没有任何评论哟~