C语言(语句底层实现)
C语言
if 与 switch 原理区别
if-else结构灵活,支持复杂的判断语句
在条件可预测的情况中,将更常触发的if放在考前位置,效率会好一点点
switch的判断条件开销均衡,但判断条件只能是类整型常量。当case数量>3个时会有优化:
其实现结构有4种,其中第二种方案是,当条件大概是线性的时:(比如条件分别是1 9 2 6 4 3 排序后:1 2 3 4 6 9)
收集每个case的代码起始地址,用他们的地址做一个表(数组),switch条件是多少,就读取表对应的内存地址,然后直接跳转到地址所在的代码区执行,直到遇到jmp(break),没有jmp跳转的话就会继续执行到相邻的下一个case(穿透),defalt可以写在多个case中间也没问题,汇编代码会按照C代码的书写顺序编写汇编指令。
红框就是建立的那个数组的基地址:
循环
执行效率最高的是do while 最低的是for
循环解决线性问题更方便,非线性问题往往用递归更容易解决
函数调用实现
数据存储是栈结构,根据栈增长方分为0地址增长(栈顶为0,到0则栈溢出,x86架构就是) 或 高地址增长
调用约定
调用方 与 被调用方需要约定:
1.参数传递方向(参数列表多个参数,是从左到右还是相反)
2.传输媒介(栈 或 寄存器)
3.返回值在哪里
4.调用方 或 被调用方 谁来清理参数占用的内存
举例:
__cdecl: 从右往左传参,传输媒介为栈,返回值在寄存器,调用方释放参数空间 (通用)
__stdcall:从右往左传参,传输媒介为栈,返回值在寄存器,被调用方释放参数空间 (通用)
__fastcall:左数前两个参数寄存器传,其他用栈传(从右往左),返回值在寄存器,被调用方释放参数空间 (微软)
int__cdeclFun(inta,intb)//返回值与函数名之间写调用约定,编辑器根据所选编译选项会自动填写,因此往往可以省略保存返回地址
保存调用方的栈信息
为将来回到调用方保存必要的信息,也就是前一个函数的栈顶
更新当前栈底到栈顶
函数执行完要回恢复到前一个栈函数的栈顶,也就是当前函数的栈底
保存调用方的栈底:将调用方的 ebp 压栈,然后将当前 ebp 指向当前栈顶。
恢复调用方的栈帧:函数返回前,将 esp 恢复为 ebp 的值,再弹出保存的调用方 ebp,使栈顶回到调用方的栈顶。
为局部变量申请空间
抬高栈顶,多的部分就是放了局部变量
保存寄存器环境
把之前函数使用的寄存器原值保存,方便将来调用结束出栈时恢复上一个函数的寄存值数据(最多只有3个 ebx, esi, edi)
/ZI /Zi 调试选项
如果启用了调试信息开关,将局部变量初始化为0xcccccccc(烫烫烫烫)
未启用则没有这一步
执行函数体
举例:main函数数据的栈结构
栈里存的都是 局部变量 和 参数变量
函数调用结束后,将以上步骤逻辑反这来一次就可以了
1.恢复原寄存器数据
2.释放局部变量空间
3.得到调用方的栈地址
4.按照不同的调用约定,相反操作
__cdecl 返回到刚刚拿到的调用方的栈地址的地方,现在就相当于主人换成了调用方,当前栈顶就是刚刚执行完成的函数返回值的地址调用方清理参数空间
其他约定就 拿到调用方地址后,先清理参数空间,再返回到调用方地址。
执行完毕
如此也能很直观的理解递归为什么占内存了,每调用一次自身,就开了一次栈空间,要是退出逻辑没写好,很容易就栈溢出了。
除了栈,数据在内存种还有以下几个分区
data区
全局变量 静态变量 常量
0x0042开始
已初始化数据区
0x0042开始
只读区
常量
可读可写区
全局变量 静态变量
vc6编译的exe会将已声明的变量内容放在exe中,程序运行时就会把该数据拿到发在程序模块基地址0040000(vc6可设基地址,必须是页边界,且不能是内核的和已占用的那些地址)
未初始化数据区
未初始化的变量在0x0046
堆
代码区
机器代码
ps: if for 后只有一行逻辑时,花括号{}可以省略,但不建议,因为当这一行是宏,而宏的内容有多行时,执行结果就不是预期的样子了。
goto 语句后边跟的标号,有时会看到两个标号相减的逻辑,这其实是在求段代码的长度。
