有关C

在论坛常看到有人问关于指针的问题,已经运行时候出错,或者程序崩溃,或者打印数据不对;或者段错误;
所以写篇文章希望对大家有用;
1;首先谈谈段;在intel处理器中逻辑地址;线性地址;物理地址;逻辑地址就是段地址+段内偏移量;在早期的8086中;逻辑地址是16位段地址×16+段内偏移量;到了后来;intel引入了所谓的实模式和保护模式;所谓实模式也是就兼容早期的8086;段地址依然是段寄存器内容;逻辑地址依然是段地址×16+段内偏移量;所谓保护模式是对于逻辑地址另外一种算法;
线性地址就是我们用段寄存器内容×16+段内偏移量算出的地址;很显然早期的地址只有20位;一种到引入MMU;就有了物理地址的和线性地址的区别;如果没有硬件MMU;那么线性地址就是时间的物理内存的地址;但是引入了MMU;这个线性地址将被MMU控制器重新解析;然后解析到不同的物理地址;这个就是所谓的分页技术;
2;谈到寄存器,我们不妨列出所有的寄存器EAX;EBX;ECX;EDX;ESP;EBP;EDI;ESI;这个8个叫通用寄存器;EIP;EFLAGS;是特殊寄存器;还有CS;DS;ES;SS;FS;GS这些段寄存器;这个里面的内容将作为段的首地址;8086的寄存器没有E;也就是说是AX;BX等;
3;指针;我们知道指针表示地址;我们也常说32bit的系统;指针长度是4字节的;那么这个4字节的内容最后到达写到哪里去呢?毫无疑问;这个指针的内容会被分为段地址和段内偏移量2个部分;段地址部分被存放到段寄存器中;(当然了保护模式下,寄存器的解析是不同于是模式的)在进一步解释指针之前;以及野指针问题;我们来看看保护模式;
4;保护模式;在现在的操作系统中都是开始用实模式;后来用保护模式;正是由于保护模式的存在;才让编程变得容易;
实模式有个缺点就是只能寻1M空间;也就是20位;对于保护模式;有一个8字节的段描述符;这个描述符描述你一个段的基本信息;包括段的起始地址;段的大小;段的颗粒大小;这里有必要说说段的大小和颗粒大小的问题;其实段的大小只是说段从起始地址开始;有多少个颗粒大小;颗粒可以是1byte;也可以是4kbyte;段就是只得颗粒的数量;段大小是20位的;如果颗粒是1byte那么段的最大就是1M;如果颗粒是4k;那么最大就是4G;
好了;那么我们一共有多少个这样的段描述符呢;换句话说我们有多少个段可以使用呢?这个时候16bit的段寄存器不再像实模式那样解释了;16bit的前13bit作为段选择符;后面3bit作为这个段的描述;这里我们不得不说GDT;LDT;GDT;和LDT;是2张表;这个2张表放着段描述符的地址;这个表有13^2个表项;好了现在我们知道所有的寄存器都有13bit的选择子;指示了选择GDT或者LDT表中段描述符的序号;比如说CS=0008H;那么我们知道;前13bit值是1;后面3bit是000;那么他将选择第2个标识符;那么GDT;LDT是什么呢?他们叫全局描述符和局部描述符;一个CPU只有1个GDT;而每个应用程序都可以有一个LDT;GDT;LTD的起始地址存放在寄存器GDTR;LDTD中;
5;说完了intel的段;我们再来看看分页机制;在linux中;并没有用到13^2个段;4个主要的段是内核的代码段;内核的数据段;应用程序代码段;应用程序数据段;他们都设置了段描述符使得其实地址是00000000H;结束地址是FFFFFFFFH;开始我们说段寄存器最后3位用作其他作用;其中一个就是设置CPU访问权限;2bit一个用4中权限;linux只用了0和3;也就是我们平时说的内核态和用户态;对于intel处理器来说如果CPU的寄存器cr0到cr4;里面有说明CPU访问权限;(我没有具体研究过)当CPU访问权限小于段的访问权限那么段是无法被访问的;也就是说CRX中如果说明CPU权限是3;而段权限是0;那么CPU是无法访问这个段的;

6;对于线性地址;处理器通过分页机制来转换到实际的物理地址;一个线性地址;被分成页目录;页表项和偏移量;所以一个页未必存放在内存中;有寄存器标志标识;所以以前有人说一个文件很大是否要申请很大的内存来存放;实际是没有必要的;因为即使你申请了大的内存;操作系统也只有在需要的时候才会给你实际的页;一个页通常是4kb的;这个时候才是真正的分配了物理内存;所以有文章说;如果你申请的数据;或者全局变量很大;可能导致程序过慢;因为你的数据分别放在2个页上;而当系统负载很高;就会导致页面不断和磁盘交互;导致程序运行过慢;这里也说明了一个野指针为什么能够通过编译;但是运行会出错;因为你的指针指示到了本来不属于这个进程的页面;而操作系统这个时候就认为你进行了非法操作;
7;linux典型的程序图;
其实这个图有很多人写过了;但是我还是想copy下;
命令行参数和环境变量


未初始化的数据
初始化的数据
正文
保留区
这里我们可以看出;所谓的正文区就是我们说的代码区;也就是指令;初始化的数据;包括初始化的全局变量;static 修饰的局部变量;未初始化区;也就是未初始化的全局变量;也就是我们所说的BSS区;堆也就是用malloc分配的;所谓的动态内存;所以这个是要释放的;栈也就是我们的自动变量(应该叫这个吧)就是那些局部的变量;命令行参数也就是我们在shell里面输入的参数;
我们每次调用函数;就会吧入口参数;也就是实际参数放在堆中;包括这个函数声明的变量;比如说
int main(int argc,char **argv)
{
  int a = 9;
  f(a);
  b=5;

}
int f(int a)
{
    int b = 6;
}
虽然我们看到int a = 9;和f(a);但是他们并不是同一个变量;我们把前一个叫a1后面一个叫a2;
在堆里面是这样分配的
a1;
a2;
b;
这个就是所谓的传值;因为c语言没有传引;C++的引用我的理解是传引;这里看到main对b赋值;这个是不能够通过的;因为当f返回堆里面的a2和b就不存在了;换句话说堆栈寄存器的偏移量已经自动减少了;论坛有文章说Windows默认的栈是1M的;但是linux我没有看到文章说对栈有大小限制;
好了;如果a是指针呢?是传值吗?还是传引?C语言只有传值换句话说即使是指针也是2个变量;所以我们常常听到有人抱怨说为什么我的输出出错了;
7;对于初学者来说;不太理解指针和指针指向的东西;因为我刚开始的时候也很迷惑;
int a = 9;
int *p =&a;
这个时候你的栈中间有个变量a;有个变量p;只不过变量p的内容是a的地址;所以有人常常
int *p;
&p= 4;
问这个为什么是错的;你的p内是一个垃圾数据;他指向了一个或者在堆;或者在栈;或者在代码区;所以这样的指针必然导致灾难的;
我们常常说栈的变量是自动释放的;而堆是不可以的必须free;我们来看下C语言对于的汇编代码;
#include <stdio.h>
#include <stdlib.h>
int f(int a,int b);
int main(int argc,char**argvs)
{
int *p = malloc (100);
int a = 5;
int b = 6;
int c = f(a,b);
free(p);
}
int f(int a,int b)
{
int c = a+b;
return c;
}
汇编代码
.file "main.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movl $100, (%esp)
call malloc
movl %eax, 28(%esp)
movl $5, 24(%esp) //a
movl $6, 20(%esp) //b 我们看到栈是向下生长的
movl 20(%esp), %eax
movl %eax, 4(%esp)//将a放到堆栈里面
movl 24(%esp), %eax
movl %eax, (%esp)//将b放到堆栈里面;这里我们明显看到C语言的传值特性;
call f  //调用f
movl %eax, 16(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call free
leave
ret
.size main, .-main
.globl f
.type f, @function
f:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 12(%ebp), %eax
movl 8(%ebp), %edx
leal (%edx,%eax), %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
.size f, .-f
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits

8;想正确使用指针;必须正确理解指针;指针的声明一句话讲完了;从变量名开始;看优先级;然后读指针含义
int (*(*f)(int,int(*)(int,float)))(int);
从f开始
1;*f//f是一个指针
2;(*f)()//f是一个函数指针
3;(*f)(int,int(*)(int,float)))//这个函数指针有2个参数int 和int(*)(int,float);后面一个依然是函数指针;
4;函数f的返回值是什么;(*(*f)(int,int(*)(int,float)))//是指针;
5;这个指针指向什么(*(*f)(int,int(*)(int,float)))(int)//一个参数为int的函数;
6;这个返回的函数指针的返回值是什么int;
所以整体是说;我声明了一个指向2有个参数,返回值是函数指针的函数指针;而2个参数是int和执行函数的指针;
知道怎么读指针了;我们再来看看多维数组
int a[3][4];
这个C语言到底表达了什么意思?有人说3×4的数组;其实这个是不准确的;应该是3个向量;每个向量有4个int元素的数组;
现在我们看看这样一段代码
      int a[3][4];
      int **p;
      p = a;
//这个代码是有警告的;说指针类型不匹配;
那么我们看看这样的情况
      int a[3][4];
    int (*p)[4];
    p = a;
//这样的代码是没有警告的;因为类型匹配了;所以我们可以看出;p实际是指向了一个4个元素的数组;所以多维数组其实是一维数组的向量;
9;数组名字到底是什么;为什么有那么多错误和数组名字有关;
      int a[4];
      a++;
      int *p = a;
      p++;
//大家知道这段代码a++是不能够通过编译的;而p++却可以;所以有很多人问这样的问题;他们都是地址;为什么不可以;
首先在声明a[4]时候;你的栈空间有4个int的空间分别给了 a[0],a[1],a[2],a[3];声明int *p;有一个空间给了p;那么你做p++;这个时候p这个空间是能够存放++之后的值的;(当然这个值未必立刻存放到这个内存中,可能暂时存放在EAX;EBX等寄存器中);而数组名a是什么;没有空间存放a;换句话说;a这个东西对于C的源程序是有用的;他告诉编译器程序员将a[0]的地址用a来表示;但是不给a分配任何空间;所以不难明白为什么对于a++是非法的了;
好了;下面我们来谈谈多维数组的名字问题;
int a[3][4];
我们不难明白a表示a[0]的地址;而a[0]是一个有4个元素的向量;那么a[0]也就是表示这个向量的数组名;因此也是一个地址;同样的道理;a[0]是不允许做++运算的;我们不能明白*a[0];表是a[0][0]的值;
而&a[0];表示a的值;但是由于a[0]和a的表示同一个值;所以我不难明白a[0]=a;至于其他情况;大家应该能够分析;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值