《汇编语言》- 读书笔记 - 综合研究
《汇编语言》- 读书笔记 - 综合研究
-
研究试验 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. 这个错误提示可能与哪个文件有关?
- 1. 在编译或连接过程中哪些环节可能会出现问题?
-
使用学习编程语言中常用的链接工具 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进行跟踪分析。
-
- 思考以下几个问题:
-
-
main函数调用指令与程序返回指令的来源是什么?
-
-
- 在没有
main函数的情况下, 错误信息中包含与cos相关的信息;而在构建开发环境时, 没有cos.obj文件中的tc.exe无法连接程序. 是否tc.exe将cos.obj文件与用户程序的.obj文件一起连接生成.exe文件?
- 在没有
-
- 用户程序中的
main函数被调用指令与其返回指令是否均来自cos.obj文件?
- 用户程序中的
-
- 我们如何查看cos.obj文件中的程序代码?
-
- cos.obj文件中包含我们所设想的代码吗?
-
- 使用link.exe将c:\minic(我的在c:\TC20\LIB)目录下的cos.obj连接,生成cos.exe.
-
- 利用Debug工具, 查找m.exe中调用main函数的call指令偏移地址,从该偏移地址开始向后查看10条指令;然后使用Debug加载cos.exe,从相同偏移地址开始向后查看10条指令. 对比两处指令序列.
-
- Turbo C编译器作为编译器连接器,将cos.obj文件与用户编写的.obj文件链接生成.exe文件的过程及其内部运行机制大致如下:
-
- 使用tc.exe重新编译f.c源代码,完成连接并生成f.exe.
-
- 在基于新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


4. 连接 Link


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

tlink 手动连接
语法:多个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。
- T模式与S共享相同的lib文件。
- 其(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:
- Real Parameters: The actual values passed to the function when it is invoked.
- Internal Variables: Variables declared within the function, which are only accessible during its execution.
- Return Address: The address of the next instruction to be executed after the function completes.
- 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 21 (4Ch带返回码方式的终止进程)这里整个程序才完全返回。
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.exe 和 m.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 即可。
- 使用masm工具生成可执行文件myprintf.obj。
- 使用编译器tcc将源文件main.c转换为main.obj。
- 使用链接器将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
