深圳龙岗建设网站,qq空间电脑版,无锡模板网站建设找哪个好,施工企业招标领导小组组长的职责文章目录 main函数很普通main函数之前调用了什么main函数和自定义函数的对比 变量名只为人而存在goto是循环的本质指针变量指针是一个特殊的数字汇编层面看指针 数组和指针数组越界问题低端地址越界高端地址越界 引用就是指针 main函数很普通
main函数是第一个被调用的函数吗在用户视角看来main函数的确程序的入口但是在CPU视角下main函数仅仅只是一个普通函数和用户自定义的其他函数没有任何的区别。
main函数之前调用了什么
Linux环境:
_start-__libc_start_main-main 每一个Linux进程的入口函数都是_start_start是一段直接由汇编语言编写的函数它负责的工作就是把程序的命令行参数以及环境变量压入栈中此时环境变量和参数一起存放在一个数组中
为了把环境变量单独提取出来_start紧接着会调用__lib_start_main函数构建一张环境变量表并进行一些全局变量的初始化工作随后再进入main函数执行用户程序再main函数退出时进行收尾操作例如全局变量的释放。这么一来main函数似乎也只不过是一个被调用的函数它只是默认被注册为用户代码的入口而已也就是说用户代码入口不一定非要是main函数
main函数和自定义函数的对比
一直以来我们编写C/C程序时都是约定俗成地添加一个main函数来启动程序(因为不这么做往往会报错)这种情况一度让不少人认为main函数具有特殊的地位能够得到CPU的青睐其实不然CPU眼中main函数啥也不是就是很普通的函数
int main(){return 0;
}
int func(){return 0;
}通过汇编观察main和func的区别会发现它们所对应的汇编指令居然完全一致
main:push rbpmov rbp, rspmov eax, 0pop rbpret
func:push rbpmov rbp, rspmov eax, 0pop rbpret2个函数所做的操作都是一样的
建立函数栈帧 push rbp / mov rbp,rsp将返回值拷入寄存器 mov eax,0释放函数栈帧并返回 pop rbp / ret
gcc有一个命令可以改变用户代码的入口使得用户指定其他函数作为程序起点 gcc -nostartfiles -efunc test.c 意思是编译test.c不使用系统的标准启动文件将程序起点设置为func函数一般不推荐这么做因为使用标准启动文件代表着你需要自己为func瞻前顾后这无疑是在自找麻烦 变量名只为人而存在
变量对程序员来说并不陌生我们无时无刻都在使用变量帮助我们记忆因为一个好的变量名可以大大提高源代码的可读性尽管如此对于可执行文件来说它并不需要存储所谓的变量名(release模式编译链接),CPU只需要知道一个逻辑地址就可进行读写操作也就是说在发布模式编译链接时所有的变量名都会被转换称逻辑地址。
因此我们可以给出关于变量的定义:变量就是逻辑地址的一个别名它向上以字符串形式以供人阅读记忆向下被转成地址值供CPU访存
int a0;
int main(){a2;return 0;
}所对应的汇编文件
main:push rbpmov rbp,rspmov DWORD PTR [rip0x0],0x2 # e main0xe//将立即数0x2写入到rip值偏移量为0的位置DWORD PTR标识4字节mov eax,0x0pop rbpret可以看出a2这条代码所对应的汇编是mov DWORD PTR [rip0x0],0x2,CPU只需要通过几个逻辑地址相对寻址就可以确定内存的哪个位置需要被赋值为2
goto是循环的本质
虽然说不鼓励在编写C/C程序时随意的使用goto但是不代表goto不值得探究早期的循环其实都是通过goto语句来实现的只不过随着程序越来越大过多的goto语句打破了程序的结构性使得源码难以维护进而衍生出了结构性更强的for、while、do语句它们都是在底层实现上都继承的goto的机制
void test_for(){for(int i0;i10;i){}
}
void test_while(){int i0;while(i10) i;
}
void test_do(){int i0;do{}while(i10);
}
void test_goto(){int i0;goto L1;
L2:if(i10) goto L1;return ;
L1:i;goto L2;
}对应的汇编代码
test_for:
//...mov DWORD PTR [rbp-4], 0jmp .L2
.L3:add DWORD PTR [rbp-4], 1
.L2:cmp DWORD PTR [rbp-4], 9jle .L3
//...
test_while:
//...mov DWORD PTR [rbp-4], 0jmp .L5
.L6:add DWORD PTR [rbp-4], 1
.L5:cmp DWORD PTR [rbp-4], 9jle .L6
//...
test_do:
//...mov DWORD PTR [rbp-4], 0
.L8:add DWORD PTR [rbp-4], 1cmp DWORD PTR [rbp-4], 9jle .L8
//...
test_goto:
//...mov DWORD PTR [rbp-4], 0jmp .L10
.L14:nop
.L10:add DWORD PTR [rbp-4], 1nopcmp DWORD PTR [rbp-4], 9jle .L14
//...除了标签值不一样外可以说基本上是一模一样 jmp指令是无条件跳转对应进入循环体 cmp指令作作比较add指令对应1 jle指令是有条件跳转负责继续or结束循环 指针变量
指针可以说是C语言的精髓所在正是因为指针使得C语言称为最灵活的高级语言它使得用户可以自由的对一个内存区域进行读写(读写是否合法是另一码事)为了更好的理解指针变量我们把指针变量这个名词拆解为指针变量变量上面提过它是地址的别名指针其实就是一个地址值因此所谓的定义一个指针变量的本质就是在一块内存空间上写入一个地址值(这和写入一个普通数没有什么区别)
指针是一个特殊的数字
地址值的本质就是数字只不过它可以被用于访存(解引用)CPU可以先通过读取存放指针的那一块内存得到其中的地址值再解引用该地址读写内存 汇编层面看指针
int a1;
int main(){int* pa;*p2;int** ppp;*pp0;return 0;
}main:
//...mov QWORD PTR [rbp-16], OFFSET FLAT:a //把a的地址写入地址[rbp-16]处QWORD PTR标识8字节mov rax, QWORD PTR [rbp-16] //读取rbp-16地址处的值放入寄存器rax(a的地址)mov DWORD PTR [rax], 2 //解引用lea rax, [rbp-16]mov QWORD PTR [rbp-8], rax mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], 0
//...无论是几级指针的解引用本质上都没有什么不同都是从一块内存中获得另一块内存的位置进行访存
数组和指针
C语言中所有的数组传参最终都会退化成指针传参因此传入多大的数组最终在一个函数内部所看到的都是一个大小固定的指针
void fun1(int arr[5]){arr[3]1;}
void fun2(char arr[100]){arr[3]1;}
void fun3(double arr[1024]){arr[3]1;}汇编代码
fun1:
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]add rax, 12 //3*4mov DWORD PTR [rax], 1
//...
fun2:
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]add rax, 3 //3*1mov BYTE PTR [rax], 1
//...
fun3:
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]add rax, 24 //3*8movsd xmm0, QWORD PTR .LC0[rip]movsd QWORD PTR [rax], xmm0
//...数组索引操作的本质就是解引用因此例子中的三个函数等价于 void fun1(int* arr){arr[3]1;} void fun2(char* arr){arr[3]1;} void fun3(double* arr){arr[3]1;} 所谓的索引操作只不过是一个偏移量用于指针的加减操作(指针变量加1减1的跨度取决于指向的类型如果是int就移动4字节char就移动1字节double则是8字节)
数组越界问题
指针作为C语言的精髓同时也是C语言最危险的一面原则上一旦获取到地址就可以进行访存但不能保证目标地址的数据是否可以被安全覆盖如果一旦不小心将一些重要的内存空间刷新就有可能导致进程崩溃甚至更严重的后果。这种行为称之为野指针非法寻址所谓野指针就是一个不应该被读写内存空间的地址野指针最容易出现的场景就是数组越界。
虽然数组越界问题很危险不过好在随着编译器进步大部分数组越界问题都能在编译阶段得到拦截。
低端地址越界
void func1(){int a[2];a[1]1;a[0]2;a[-1]3;a[-2]4;
}
void func2(){int b[4];b[3]1;b[2]2;b[1]3;b[0]4;
}
int main(){func1();printf(have a good day\n);return 0;
}很明显func1中存在数组越界访问的问题但是它可能可以运行不会有段错误(可以看到have a good day,高版本编译器在编译阶段就直接报错),之所以正常运行的原因是因为虽然func1对一个非法区域进行了写入操作但是碰巧这一块区域没有任何有效数据所以不会出错
汇编代码
func1:
//...mov DWORD PTR [rbp-4], 1mov DWORD PTR [rbp-8], 2mov DWORD PTR [rbp-12], 3mov DWORD PTR [rbp-16], 4
//...
func2:
//...mov DWORD PTR [rbp-4], 1mov DWORD PTR [rbp-8], 2mov DWORD PTR [rbp-12], 3mov DWORD PTR [rbp-16], 4
//...如果编译可以通过通过查看汇编代码可以知道-1、-2索引操作偷偷地拓展了数组的长度使之变成4并且是向低地址拓展的(rbp保存栈底指针栈向低地址增长)这种越界称为低端地址越界它可能不会造成程序崩溃但大概率会影响到结果正确性(因为func1修改了本不属于它的栈空间这可能造成其他局部变量数据失效)
高端地址越界
为了更好地就是高端地址越界需要先稍微了解一下函数栈帧概念每一个函数有一个栈空间称为栈帧函数中所需要的局部变量都存储在栈帧中当被调函数返回时需要销毁栈帧CPU的指令寄存器恢复至主调函数。因此在建立新的函数栈帧时必须保存当前的指令地址以供后续返回这个返回地址通过紧邻着被调函数栈帧的栈底。如果被调函数意外的修改了这里的值就会发生意外(意外地执行恶意代码或段错误)这种越界称为高端地址越界
void evil(){//恶意代码printf(evil\n);exit(1);//让进程意外结束
}
void func1(){int a[2];a[2](int)evil; //将返回值设置为一个恶意函数//a[2]0,将返回值设置为0地址处这里没有合法指令就报段错误a[1]1;a[0]2;
}
int main(){func1();printf(have a good day\n);return 0;
}(如果编译可以通过)main函数调用func1后进程就结束了没有输出have a good day即func1回不到main了转而走到evil
引用就是指针
C中引入的引用是对指针操作的简化但其实只是在语法层面上做了一层封装和少量限制(没有多级引用和空引用引用二次引用其他对象)
void f1(int* x){*x1;}
void f2(int x){x1;}
void f3(int x){x1;}汇编代码
f1(int*):
//...建立栈帧mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]mov DWORD PTR [rax], 1
//...释放栈帧
f2(int):
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]mov DWORD PTR [rax], 1
//...
f3(int):
//...mov QWORD PTR [rbp-8], rdimov rax, QWORD PTR [rbp-8]mov DWORD PTR [rax], 1
//..通过汇编可以很明显的看到f1和f2的赋值操作完全一致引用也是通过获得地址后解引用才能实现对外部变量的修改,左值引用和右值引用在汇编实现上没有什么区别只不过对于右值引用的修改是针对于一个被延长声明周期的临时对象做修改。
一句话描述引用引用变量就是一个指针对于引用变量的修改就是解引用操作
————————————————————————————