精华内容
下载资源
问答
  • 阅读下面程序,并写出运行结果

    千次阅读 2016-05-26 09:05:32
    * 文件名称:第12周项目:阅读下面程序,并写出运行结果 * 作者:马康泰 * 完成日期:2016.5.19 * 版本号:v1.0 * * 问题描述:阅读下面程序,并写出运行结果 * 输入描述: * 程序输出: */ //虚...
    /* 
     * Copyright(c)2016,烟台大学计算机与控制工程学院 
     * All rights reserved. 
     * 文件名称:第12周项目:阅读下面的程序,并写出运行结果
     * 作者:马康泰
     * 完成日期:2016.5.19 
     * 版本号:v1.0 
     * 
     * 问题描述:阅读下面的程序,并写出运行结果 
     * 输入描述: 
     * 程序输出: 
    */  
     //虚函数
     #include <iostream>
     using  namespace  std;
     class  A  {
     	int a;
     public:
     	A():a(5){
     		virtual void print()const{cout<<a;}
     	};
     	class  B:  public  A {
    char  b;
    public:
    B()  {  b='E';
    void  print()  const  { cout<<b;  }
    };
    void shoow(A &x){x.print();}
    int main()
    {
    	A d1,*p;
    	B d2;
    	p=&d2;
    	d1.print;
    	d2.print;
    	p->print;
    	show(d1);
    	show(d2);
    	return 0;
    }
    <img src="https://img-blog.csdn.net/20160526090431669?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
    //(2)虚析构函数
    #include  <iostream>
    using  namespace  std;
    class  BASE
    {
    	private:
    		char  c;
    	public:
    		BASE(char  n):c(n)  {}
    virtual  ~BASE()  {  cout<<c; }
    };
    class  DERIVED:public  BASE
    {
    private:
    	char  c;
    	public:
    		DERIVED(char  n):BASE(n+1),c(n)  {}
    ~DERIVED(){  cout<<c;  }
    };
    int  main(){
    DERIVED  d('X');
    return  0;
    }
    <img src="https://img-blog.csdn.net/20160526090437812?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
    //(3)纯虚函数
    #include  <iostream>
    using  namespace  std;
    class  Base
    {
    	public:
    		virtual  void  Who()  =0;
    };
    class  FirstDerived:public  Base
    {
    public:
    void  Who()  {  cout<<"F";  }
    };
    class  SecondDerived:public  Base
    {
    public:
    	void  Who()  {  cout<<"S";  }
    };
    int  main()
    {
    FirstDerived  first_obj;
    SecondDerived  second_obj;
    Base  &bref=first_obj;
    bref.Who();
    bref=second_obj;
    bref.Who();
    Base  *bp;
    bp=&first_obj;
    bp->Who();
    bp=&second_obj;
    bp->Who();
    return  0;
    }
    <img src="https://img-blog.csdn.net/20160526090447622?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
    


    展开全文
  • 【深入理解计算机系统】程序是如何运行

    万次阅读 多人点赞 2017-11-05 20:53:14
    程序是如何运行的现代计算机存储和处理的信息以二值信号表示,程序对于计算机而言就是一串二进制数据流,以流水线的方式进入CPU进行运算。主要在;CPU与内存之间进行数据传递。本文将从程序源码的结构与表现形式开始...

    程序是如何运行的

    现代计算机存储和处理的信息以二值信号表示,程序对于计算机而言就是一串二进制数据流,以流水线的方式进入CPU进行运算。而数据主要在CPU与内存之间进行传递。本文将从程序源码的结构与表现形式开始,源代码编译成可执行文件,再到执行文件的加载,最终到执行文件的运行整个过程进行梳理。

    ###1 程序的结构与表现形式
    大多数计算使用8位的块,即字节(byte),作为最小的可寻址的内存单元。程序对象,即程序数据、指令和控制信息的字节集合,编译器和系统运行时将存储空间划分成更可管理的单元来存储程序对象。

    计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理内存、读写存储设备上的数据、以及利用网络通信。程序源码会经过编译器生成机器代码,编译器基于编程语言的规则、目标机器的指令集合和操作系统遵循的惯例,经过一系列的阶段生成机器代码。汇编代码是机器代码的文本表示,给出程序中的每一条指令。

    计算机系统使用了多种不同形式的抽象,利用抽象模型来隐藏实现的细节。对于机器级编程来说,两个重要的抽象:

    1. 指令集架构(Instruction Set Architecture, ISA) 定义机器级别格式和行为,处理器状态、指令的格式,以及每条指令对状态的影响。
    2. 虚拟内存地址,程序使用的内存地址是虚拟地址,提供内存模型看上去是一个非常大的字节数组。实际上由许多个硬件存储器和操作系统软件组合起来。

    以C语言为例,编写程序mstore.c

    long mult2(long, long);
    
    void multistore(long x, long y, long *dest) {
    	long t = mult2(x, y);
    	*dest = t;
    } 
    

    经过gcc编译器,产生一个汇编文件mstore.s

    multstore:
    	pushq %rbx
    	movq %rdx, %rbx
    	call mult2
    	movq %rax, (%rbx)
    	popq %rbx
    	ret
    

    上面代码中每行对应一条机器指令,比如, pushq指令应该将%rbx的内容压入程序栈中。

    再将mstore.c编译并汇编成目标代码文件mstore.o,该二进制文件中,有一段14个字节的序列,它的十六进制表示为:

    53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3
    

    为了弄清这些14个字节表示的含义,可以通过objdump 反汇编 该mstore.o 文件
    这里写图片描述

    可以看到,这14个字节分成若干组,左边是一条指令,右边是等价的汇编语言。

    程序中包含过程、控制

    过程
    是软件中一种重要的抽象。它提供了一种封装代码的方式,用一组制定的参数和一个可选的返回值实现了某一功能。然后,可以在程序中不同的地方调用这个函数。设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接口定义,说明计算的是哪些值,过程会对程序状态产生什么样的影响。不同编程语言中,过程的形式多样;函数(funciton)、方法(method)、子例程(subroutine)、处理函数(handler)等等。

    要提供对过程的机器级支持,必须要处理许多不同的属性。为了讨论方便,假设过程P调用过程Q,Q执行后返回到P。这些动作包括以下一个或多个机制:

    1. 传递控制。在进入过程Q的时候,程序计数器必须被设置位Q的代码的起始地址,然后在返回时,要把程序计数器设置位P中调用Q后面那条指令的地址。
    2. 传递数据。P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值
    3. 分配和释放内存。在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。

    x86-64的过程实现包括一组特殊的指令和一些对机器资源(寄存器和程序内存)使用的约定规则。

    控制
    程序中的控制逻辑,例如条件语句if else, 循环for do-while等。机器级指令的执行,有两种方式实现条件控制,一种将控制条件进行传递,一种是将不同条件计算结构进行传递。后一种方式在现代计算机中能提高程序运行的效率,代码中的指令都是按照在程序中出现的次序,顺序执行的,使用jump指令可以改变一组机器代码指令的执行顺序,从而实现条件操作。

    为了实现条件控制,CPU中维护了一组单个位的条件码(condition code) 寄存器,它们描述了最近的算数或逻辑操作的属性。可以通过检测这些寄存器来执行条件分支指令,通常条件码有,CF:进位标志;ZF:零标志。SF: 符号标志;OF:溢出标志。

    运行时调用栈
    这里写图片描述
    大多数语言过程调用机制采用栈数据结构提供的后进先出的内存管理原则。过程P调用过程Q的过程,如果上图所示。

    ###2 程序代码的编译过程
    这里写图片描述

    预处理阶段,主要是修改原始程序,例如将#include<stdio.h> 命令告诉预处理读取系统stdio.h的文件,并将它直接插入到程序文本中。结果得到的另一个C程序,以.i作为扩展名;
    编译阶段,编译器ccl将文本hello.i翻译成文本hello.s,它包含一个汇编语言程序;
    汇编阶段,汇编器将.s文件编译成一个二进制的文件,把这些指令打包成一种叫做可重定位的目标程序的格式,并将结果保存在目标文件.o文件中。
    链接阶段,将各种代码和数据片段手机并组合并成可以执行的目标文件,简称可执行文件,可以被加载到内存中,由系统执行。

    本节主要讨论链接阶段。链接可以执行与编译时,即将源代码翻译成机器代码时;可以执行与加载时,即程序被加载器加载到内存时;可以执行与运行时,也就是由应用程序来执行。

    链接器是的分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织成一个巨大的源文件,而时可以把它分解为更小、更好管理的模块。理解链接的工作原理可以帮助我们避免一些危险的编程错误、理解语言的作用域规则、理解一些重要概念(加载、运行程序、虚拟内存、分页、内存映射)、有助于理解共享库。

    为了构造可执行的文件,链接器必须完成两个主要任务:

    1. 符号解析(symbol resolution)。 目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的时将每个符号引用正好和一个符号定义关联起来;
    2. 重定位(relocaiotn)。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得他们指向内存这个内存位置。链接器使用汇编器产生的重定位条目(relocation entry)的详细指令,不加甄别地执行这样的重定位。

    目标文件分为三种:

    1. 可重定位目标文件,包含二进制代码和数据,其形式在编译时可以与其他可重定位目标文件合并起来,创建一个可执行目标文件;
    2. 可执行目标文件,包含二进制代码和数据,其形式可以被直接复制到内存并执行;
    3. 共享目标文件,一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

    ###3 执行文件

    可执行的目标文件,通过加载器,加载到内存,供CPU调用运行。

    进程是执行中程序的一个具体实例,程序总是运行在某个进程的上下文中。

    进程提供了给应用程序的关键抽象:

    1. 一个独立的逻辑控制流,程序计数器PC值序列叫做逻辑控制流,每个PC值对应可执行目标文件中的指令,或者是包含在运行是动态链接到程序的共享对象中的指令。
    2. 一个私有的地址空间,进程位每个程序提供一种假象,好像它独占地使用系统地址空间。例如,在一台64位地址的机器上,地址空间是 2 6 4 2^64 264 个可能地址的集合。进程为每个程序提供它自己的私有地址空间。一般而言,其他进程是不能访问该进程的地址空间所关联的内存字节。
      每个私有地址空间有相同通用的结构,如下图所示
      这里写图片描述
      地址空间底部是保留给用户程序的,包括通常的代码、数据、堆和栈段。代码段总是从地址0x00400000开始。地址空间顶部保留给内核(操作系统常驻内存的部分)。地址空间的这部分包含内核在代表进程执行指令时使用的代码、数据和栈。

    为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存器的一个模式为来提供这种功能,该寄存器描述了进程当前享有的特权。

    进程运行有两种模式:

    1. 内核模式(超级用户模式)
    2. 用户模式

    当设置了模式位时,进程就运行在内核模式中(超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统的任何内存位置。

    没有设置模式位时,进程运行在用户模式中。用户模式中的进程不允许执行特权指令(privileged instruction),比如停止处理器、改变模式位、或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。

    操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文明。上下文就是内核重新启动一个被抢占的进程所需状态。进程切换包含三个操作

    1. 保存当前进程的上下文
    2. 恢复某个先前被抢占的进程被保存的上下文
    3. 将控制传递给这个新恢复的进程

    这里写图片描述

    当进程A开始读取磁盘文件时,会通过执行系统调用read陷入到内核。内核中的陷进处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘终端处理器。

    每个执行的程序,即系统中的进程,进程总可以处于下面三种状态:

    1. 运行,进程要么在CPU上执行,要么在等待被执行且最终会被内核调度;
    2. 停止,进程的执行被挂起,且不会被调度,当收到SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOU信号时,进制就会停止,并且 保持停止知道它收到一个SIGCONT信号,在这个时刻,进程再一次开始运行。
    3. 终止,进程永远地停止了。进程会因为三种原因终止:1)收到进程终止的信号,2)从主程序返回;3)调用exit函数。

    加载并运行程序

    当使用execve函数在当前进程的上下文中加载并运行一个新程序。
    (fork是在父进程下,创建一个新的上下文运行子进程)

    #include <unistd.h>
    int execve(const char *filename, const char *argv[], const char *envp[]);
    

    当使用execve加载filename之后,启动代码设置栈,并将控制传递给新程序的主函数。
    用户栈的典型组织结构:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aIstAqiI-1619105227349)(https://img-blog.csdn.net/20171105200816854?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ2RwMTIzMTVfZ3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]

    信号
    除了操作系统利用异常来支持进程上下文切换的异常控制流形式,另外一种更高层次的软件形式的异常,成为Linux信号,它运行进程和内核中断其他进程。

    一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件。底层的硬件异常是由内核异常处理程序处理的,正常情况下,对于用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。比如,当进程在前台运行时,你键入Ctrl+C,那么内核就会发送一个SIGINT信号强制终止它。当一个子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。

    传送一个信号到目的进程是由两个不同步骤组成的

    1. 发送信号,内核通过更新目的进程上下文种的某个状态,发送一个信号给目的进程。发送信号可以由如下两种原因:1)内核检测到一个系统事件,比如除零错误。2)一个进程调用kill,显示要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己
    2. 接受信号,当目的进程被内核强波以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过之心一个称为信号处理程序的用户曾函数捕获这个信号。

    一个发出而没有被接收的信号,叫做处理信号,在任何时刻,一种类型至多只会由一个待处理信号。重复发送在等待的信号,将会被内核抛弃。
    linux 提供两种阻塞机制,隐式和显式

    1. 隐式,即内核默认会阻塞当前处理程序接受到的待处理信号,正好与该待处理信号类型相同的信号已经被该处理程序所捕获。
    2. 显示阻塞机制,应用程序可以使用singprocmask函数和它的辅助函数,明确地阻塞和解除阻塞选定的信号。

    通过本文我们阐述了,程序在计算机种运行的一些基本概念、逻辑流、内存等。然后,系统种程序往往不是独立运行的,不仅仅是包含最小限度的输入和输出。在现实世界中,应用程序利用操作系统提供的服务来与I/O设备以及其他程序通信。

    下一篇,详细探讨Unix操作系统提供的基本I/O服务,以及如何用这些服务来构造应用程序,例如Web客户端和服务器。

    展开全文
  • 程序是如何运行

    千次阅读 2018-11-22 12:33:18
    对于任何一个学习过C语言的来说,“HelloWorld”程序都不会陌生。因为它应该是你打开新...但是你们知道短短的几行代码,是怎么让程序运行起来的么? // hello.c #include &lt;stdio.h&gt; int main(int a...

    转载https://www.jianshu.com/p/7d11045a40f8

    对于任何一个学习过C语言的来说,“HelloWorld”程序都不会陌生。因为它应该是你打开新世界的看到的第一束光。至今我还记得第一次敲出这个程序的时候激动了好久。但是你们知道短短的几行代码,是怎么让程序运行起来的么?

    // hello.c
    #include <stdio.h>
    int main(int argc, char *argv[]) {
    printf(“Hello World!\n”);
    return 0;
    }
    程序是如何运行起来的?很多人可能会说,不就是五个步骤:预处理(Prepressing),编译(Compilation),汇编(Assembly)和链接(Linking),装载(Loading)么?是这样的。但是你知道每一步背后都做过一些什么吗?如果你能回答上以下的问题,我想这个文章就没有必要看下去了。

    在main()函数调用之前,程序做过一些什么?

    编译出来的可执行文件里面有什么,在内存中是什么样子的,是怎么来组织的?

    静态链接、动态链接,有什么区别?

    不同的编译器(Micrsoft VC/VS, GCC)和不同的硬件平台(X86,SPARC,MIPS,ARM),以及不同的操作系统(Windows,Linux,Unix,Solaris),最终编译出来的结果一样么?

    ELF文件,PE文件,COFF文件,是什么?

    如果你发现对其中的一些问题,不是很了解的话,甚至没有想过这些问题的时候,而你有向了解一下,那么就可以,跟着我的步伐一步俩步,往下看啦。这个文章是为你准备的。需要声明的是,本文主要针对gcc编译器,也就是针对C和C++,不一定适用于其他语言的编译。下图为总览。

    GCC编译过程
    预处理
    预处理的过程,其实,主要是处理那些源代码中以#开始的预编译指令。比如#include,#define等,处理的规则如下:

    将所有的#define删除,并且展开所有的宏定义

    处理所有的条件预编译指令,比如#if, #ifdef, #elif, #else, #endif等

    处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。在这个插入的过程中,是递归进行的,也就是说被包含的文件,可能还包含其他文件。

    删除所有注释 //和/**/.

    添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。

    保留所有的#pragma编译器指令,因为编译器需要使用它们。

    对于第一步预编译的过程,可以通过以下方式完成:

    gcc -E hello.c -o hello.i
    或者

    cpp hello.c > hello.i
    编译
    编译过程可分为6步:词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化。对应与下图的每一步。下面我们以一个具体的表达式进行分析:

    array[index] = (index + 4)*(2 + 6);
    Compilation
    词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。
    记号 类型
    array 标记符
    [ 左方括号
    index 标记符
    ] 右标记符
    = 赋值
    ( 左圆括号
    index 标记符

    • 加号
      4 数字
      ) 右圆括号
    • 乘号
      ( 左圆括号
      2 数字
    • 加号
      6 数字
      ) 右圆括号
      注:lex工具,可实现按照用户描述的词法规则将输入的字符串分割为一个一个记号。

    语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。

    Syntax Tree

    注:yacc工具(yacc: Yet Another Compiler Compiler)可实现语法分析,根据用户给定的语法规则对输入的记号序列进行解析,从而构建一个语法树,所以它也被称为“编译器编译器(Compiler Compiler)”。

    语义分析:编译器所分析的语义是静态语义,所谓静态语义就是指在编译期可以确定的语义,通常包括声明,和类型的匹配,类型的转换。

    Commented Syntax Tree

    注:与之对于的为动态语义分析,只有在运行期才能确定的语义。

    源代码优化:源代码优化器(Source Code Optimizer),在源码级别进行优化,例如(2 + 6)这个表达式,其值在编译期就可以确定。优化后的语法树。
    Paste_Image.png

    但是直接作用于语法树比较困难,所以源代码优化器往往将整个语法数转化为中间代码(Intermediate Code)。
    注:中间代码是与目标机器和运行环境无关的。中间代码使得编译器被分为前端和后端。编译器前端(1-4步)负责产生机器无关的中间代码;编译器后端(5-6步)将中间代码转化为目标机器代码。

    目标代码生成:代码生成器(Code Generator)。

    目标代码优化:目标代码优化器(Target Code Optimizer)。

    最后的俩个步骤十分依赖与目标机器,因为不同的机器有不同的字长,寄存器,整数数据类型和浮点数据类型等。

    汇编
    汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,所以根据汇编指令和机器指令的对照表一一翻译即可。汇编过程可以通过以下方式完成。

    as hello.s -o hello.o
    或者

    gcc -c hello.s -o hello.o
    链接
    静态链接
    把一个程序分割为多个模块,然后通过某种方式组合形成一个单一的程序,这就是链接。而模块间如何组合的问题,归根到底,就是模块如何进行通信的俩个问题:(1) 模块间的函数调用,(2) 模块间的变量访问。但无论是那一个问题,其本质是获取一个地址,函数运行的地址、或者变量存放的地址。

    如果熟悉汇编的,应该会知道hello.o文件,既目标文件,是以分段的形式组织在一起的。其简单来说,把程序运行的地址划分为了一段一段的片段,有的片段是用来存放代码,叫代码段,这样,可以给这个段加个只读的权限,防止程序被修改;有的片段用来存放数据,叫数据段,数据经常修改,所以可读写;有的片段用来存放标识符的名字,比如某个变量 ,某个函数,叫符号表;等等。由于有这么多段,所以为了方便管理,所以又引入了一个段,叫段表,方便查找每个段的位置。

    当文件之间相互需要链接的时候,就把相同的段合并,然后把函数,变量地址修改到正确的地址上 。这就是静态链接,如下图。

    静态链接
    但是这里有俩个问题:

    对于计算机的内存和磁盘的空间浪费比较严重

    想想一下,现在一个静态库,至少都是1MB以上。但是假如有1000个或者更多的程序在链接的时候,都静态链接了它,那么当这些程序运行起来的时候,内存中就会存在1000+相同的副本,还是一模一样的。这样,至少1GB空间就浪费了。

    程序的更新,部署,和发布会带来很多麻烦

    比如一个程序Program所使用的Lib.o是使用的第三方厂商提供的,那么当该厂商更新了Lib.o(比如修复了一个bug,或者优化了性能),那么Program的厂商就必须要拿到最新版的Lib.o,然后与Program.o链接。将新的Program发给用户。这样,一旦程序任何位置有一个小小的改动,都会导致重新下载整个程序。

    动态链接
    我们的想法很简单,就是当第一个例子在运行时,在内存中只有一个副本;第二个例子在发生时,只需要下载更新后的lib,然后链接,就好了。那么其实,这就是动态链接的基本思想了:把链接这个过程推迟到运行的时候在进行。在运行的时候动态的选择加载各种程序模块,这个优点,就是后来被人们用来制作程序的插件(Plug-in)。

    这里,我们不得不介绍一个东西,叫做动态链接器。它会在程序运行的时候,把程序中所有未定义的符号(比如调了动态库的一个函数,或者访问了一个变量)绑定到动态链接库中。简单的来说就是把程序中函数的地址改正到动态库,之后动态链接器会把控制权交给程序,然后程序执行。

    这种在装载时修正地址,经常被称为装载时重定位(Load Time Relocation)。而静态链接时修正,则被称为链接时重定位(Link Time Relocation)。

    可能有的人,就要问了,多个程序应用一个库不会有问题么?变量冲突?是这样的。动态链接文件,把那些需要修改的部分分离了出来,与数据放在了一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这种方案就是目前被称为地址无关代码(PIC,Position-independent Code)的技术。

    链接库
    通过上面,我们了解到了动态链接,静态链接。一组相应目标文件的集合,我们称它为库。因而也就有了静态链接库,动态链接库。

    静态链接库:在Linux平台上,常以.a或者.o为拓展名的文件,我们最常用的C语言静态库,就位于/usr/lib/libc.a;而在Windows平台上,常以.lib为拓展名的文件,比如Visual C++附带的多个版本C/C++运行库,在VC安装的目录下的lib\目录。

    动态链接库:在Linux平台上,动态链接文件为称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象。他们一般常以.so为拓展名的文件;而在Windows平台上,动态链接文件被称为动态链接库(DLL,Dynamical Linking Library),通常就是我们常见的.dll为拓展名的文件。

    装载
    介绍装载就不得不介绍三种文件格式了:ELF,PE,COFF。现在PC平台上流行的可执行文件格式(Executable),无论是Windows下的PE(Portable Executable)文件,还是Linux下的ELF(Executable Linkable Format)文件,都是COFF(Common file format)文件格式的变种。可执行文件例如,Windows下的*.exe,Linux下的/bin/bash。其实目标文件,内部结构上来说和可执行文件的结构几乎是一样的,所以一般跟可执行文件格式一起用一种格式进行存储。

    下面以ELF文件为例子,介绍。

    每一个ELF文件,都会有一个ELF文件头,里面会记录很多关于这个程序相关信息,通过它确定段表,进而确定各个段。总的来说,装载做了以下三件事情:

    创建虚拟地址空间

    读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系

    将CPU的指令寄存器设置为运行库的初始函数(初始函数不止一个,第一个启动函数为:_start),初始了main()函数的环境,然后指向可执行文件的入口

    作者:Torival
    链接:https://www.jianshu.com/p/7d11045a40f8
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • C语言程序运行

    千次阅读 2016-12-28 21:46:54
    一、程序运行类型(下面有详细介绍) 在嵌入式系统中,经过编译的C语言程序可以通过操作系统运行,也可以在没有操作系统的情况下运行程序存放的位置和运行的位置通常是不一样的。一般情况下,经过编译后的程序...

    一、程序的运行类型(下面有详细介绍)

    在嵌入式系统中,经过编译的C语言程序可以通过操作系统运行,也可以在没有操作系统的情况下运行。程序存放的位置和运行的位置通常是不一样的。一般情况下,经过编译后的程序存储在Flash或者硬盘中,在运行时需要将程序加载到RAM中。嵌入式系统的Nor Flash和硬盘还有一定的差别,在硬盘的程序必须加载到RAM中才可以运行,但是在Nor Flash中的程序可以通过XIP(eXcutive In Place)的方式运行。

    在嵌入式系统中,C语言程序的运行包括3种类型:第一种是调试阶段的程序运行,这个阶段程序存放的位置和运行的位置是相同的;第二种是程序直接在Flash中运行(XIP);第三种是将Flash或者硬盘中的程序完全加载到RAM中运行。

    二、程序的运行空间及内容

    在C语言程序的运行中,存在着两个基本的内存空间,一个是程序的存储空间,另一个是程序的运行空间。程序的存储空间必须包括代码段、只读数据段和读写数据段,程序的加载区域必须包括读写数据段和未初始化数据段。

    由于程序放入系统后,必须包括所有需要的信息,代码表示要运行的机器代码,只读数据和读写数据包含程序中预先设置好的数据值,这些都是需要固化存储的,但是未初始化数据没有初值,因此只需要标示它的大小,而不需要存储区域。在程序运行的初始化阶段,将进行加载动作,其中读写数据和未初始化数据都是要在程序中进行"写"操作,因此不可能放在只读的区域内,必须放入RAM中。当然,程序也可以将代码和只读数据放入RAM。在程序运行后,堆和栈将在程序运行过程中动态地分配和释放。

    三、C语言可执行代码结构        

    一般情况下一个可执行二进制程序(更确切的说在Linux操作系统下为一个进程单元,在UC/OSII中被称为任务)。在存储(没有调入到内存运行)时拥有3个部分,分别是代码段(text)、数据段(data)和BSS段。这3个部分一起组成了该可执行程序的文件。

    (1)代码段(text segment) 存放CPU执行的机器指令。通常代码段是可共享的,这使得需要频繁被执行的程序只需要在内存中拥有一份拷贝即可。代码段也通常是只读的,这样可以防止其他程序意外地修改其指令。另外,代码段还规划了局部数据所申请的内存空间信息。         

    (2)数据段(data segment) 或称全局初始化数据段/静态数据段(initialized data segment/datasegment)。该段包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据。        

    (3)未初始化数据段 亦称BSS(Block Started by Symbol)。该段存入的是全局未初始化变量、静态未初始化变量。

    而当程序被加载到内存单元时,则需要另外两个域:堆域和栈域。一个正在运行的C程序占用的内存区域分为代码段、初始化数据段、未初始化数据段(BSS)、堆、栈5个部分。

    (4)栈段(stack) 存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。      

    (5)堆段(heap) 用于动态内存分配,即使用malloc/free系列函数来管理的内存空间。

    四、C语言程序的段

    根据C语言的特点,每一个源程序生成的目标代码将包含源程序所需要表达的所有信息和功能。目标代码中各段生成情况如下:

    1.代码段(Code

    代码段由程序中的各个函数产生,函数的每一个语句将最终经过编译和汇编生成二进制机器代码(具体生成哪种体系结构的机器代码由编译器决定)。

    顺序代码

    基本数学运算(+,-),逻辑运算(&&,||),位运算(&,|,^)等都属于顺序代码。

    选择代码

    if,if…else语句等将由编译器生成选择代码。

    循环代码

    while(),do…while()语句等将由编译器生成循环代码。

    对于一些较为复杂的数学运算如除法(\),取余(%)等,虽然它们是C语言的基本运算,但在各种编译系统中的处理方式却不一定相同。根据编译器和体系结构的特点,对它们的处理方式有可能与加减等运算相同,即直接生成处理器的机器代码,也有可能转换成一个库函数的调用。例如,在没有除法指令的体系结构中,编译器在编译a/b这类除法运算的时候,由于处理器没有与其对应的指令,因此会使用调用库函数来模拟除法运算。浮点数的处理与之类似:对于支持浮点运算的体系结构,将直接生成浮点代码;对于不支持浮点数的处理器,编译器将会把每一个浮点运算用库函数调用的方式模拟。

    2.只读数据段(ROData

    只读数据段由程序中所使用的数据产生,该部分数据的特点是在运行中不需要改变,因此编译器会将该数据放入只读的部分中。C语言的一些语法将生成只读数据段。

    只读全局量

    例如:定义全局变量const chara[100]={"ABCDEFG"}将生成大小为100个字节的只读数据区,并使用字串"ABCDEFG"初始化。如果定义为const chara[]={"ABCDEFG"},没有指定大小,将根据"ABCDEFG"字串的长度,生成8个字节的只读数据段。

    只读局部量

    例如:在函数内部定义的变量const charb[100] ={"9876543210"};其初始化的过程和全局量一样。

    程序中使用的常量

    例如:在程序中使用printf("information\n"),其中包含了字串常量,编译器会自动把常量"information \n"放入只读数据区。

    在const chara[100]={"ABCDEFG"}中,定义了100个字节的数据区,但是只初始化了前面的8个字节(7个字符和表示结束的'\0')。在这种用法中,实际后面的字节没有初始化,但是在程序中也不能写,实际上没有任何用处。因此,在只读数据段中,一般都需要做完全的初始化。

    3.读写数据段(RWData

    读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段。这部分数据段和代码段,与只读数据段一样都属于程序中的静态区域,但是具有可写的特点。

    已初始化全局变量

    例如:在函数外部,定义全局的变量chara[100]={"ABCDEFG"}

    已初始化局部静态变量

    例如:在函数中定义static charb[100] ={"9876543210"}。函数中由static定义并且已经初始化的数据和数组将被编译为读写数据段。

    读写数据区的特点是必须在程序中经过初始化,如果只有定义,没有初始值,则不会生成读写数据区,而会定位为未初始化数据区(BSS)。

    4.未初始化数据段(BSS

    未初始化数据段常被称之为BSS(英文Block Start by Symbol的缩写)。与读写数据段类似,它也属于静态数据区,但是该段中的数据没有经过初始化。因此它只会在目标文件中被标识,而不会真正称为目标文件中的一个段,该段将会在运行时产生。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。

    const char ro[]={"this is readonly data"}; /* 只读数据段 */
    static char rw1[]={"this is global readwrite data"}; 
     /*
    已初始化读写数据段 */
    char bss_1[100];       /*
    未初始化数据段 */
    const char* ptrconst = "constant data"; /* "constant data"
    放在只读数据段 */
    int main()
    {
    short b;       /* b
    放置在栈上,占用2个字节 */
    char a[100];    /*
    需要在栈上开辟100个字节,a的值是其首地址 */
    char s[] = "abcde"; /* s
    在栈上,占用4个字节 */
    /* "abcde "
    本身放置在只读数据存储区,占6字节 */
    char *p1;      /* p1
    在栈上,占用4个字节  */
    char *p2 = "123456";/* "123456"
    放置在只读数据存储区,占7字节 */
      /* p2
    在栈上,p2指向的内容不能更改。 */
    static char rw2[]={"this is local readwrite data"};  
    /*
    局部已初始化读写数据段 */ 
    static char bss_2[100];   /*
    局部未初始化数据段 */ 
    static int c = 0;     /*
    全局(静态)初始化区 */
    p1= (char *)malloc(10*sizeof(char));
    /*
    分配的内存区域在堆区。 */
    strcpy(p1, "xxxx");    /* "xxxx"
    放置在只读数据存储区,占5字节 */
    free(p1);      /*
    使用free释放p1所指向的内存 */
    return 0;
    }

    连接器将根据连接顺序将各个文件中的代码段取出,组成可执行程序的代码段,只读数据段和读写数据段。在连接过程中,如果出现符号重名、符号未定义等问题,将会产生连接错误。如果连接成功,将会生成一个统一的文件,这就是可执行程序。

    实质上,在目标文件(*.o中未初始化数据段和读写数据段的区别也在于此:读写数据段占用目标文件的容量,而未初始化数据段只是一个标识,不需要占用实际的空间。(但是这个标示存在哪呢,应该占用一些空间)未初始化数据段(BSS)将在程序的初始化阶段中开辟

    知识点:在连接过程之前,各个源文件生成目标文件相互没有关系。在连接之后,各目标文件函数和变量可以相互调用和访问,从而被联系在一起。

    例如,在某一个C语言的源程序文件中,具有以下的内容:

    static char bss_data[2048];
    static char rw_data[1024] = {""};

    以上定义了两个静态数组,由于bss_data没有初始化,是一个未初始化数据段的数组,编译器只需要标识它的大小即可,而rw_data已经有了一定的初始化数据(即使这个初始化数据没有实际的内容),它建立在已初始化数据段之上,编译器需要在读写数据段内为其开辟空间并赋初值。因此,在生成目标文件的时候,由于rw_data[1024]的存在,目标文件的大小将增加1024字节,而bss_data [2048]虽然定义了2048字节的数组,目标文件的大小并不会因此而增加。

    五、C语言程序的运行总结

    在具有操作系统的情况下,程序由操作系统加载运行,加载的时候可执行程序可以是一个文件,这个文件将包含程序的主要段以及头信息。对于Linux操作系统,目标程序是可执行的ELF(Executable and linking Format)格式,对于需要在系统直接运行的程序,目标程序应该是纯粹的二进制代码,载入系统后,直接转到代码区地址执行。

    事实上,无论运行环境如何,C语言程序在运行时所进行的动作都是类似的。程序在准备开始运行的时候,以下几个条件都是必不可少的:

    1.代码段必须位于可运行的存储区。

    2.读写数据段必须在可以读写的内存中,而且必须经过初始化。

    3.未初始化数据段必须在可以读写的内存中开辟,并被清空。

    对于第1点,代码段如果位于可以运行的存储区域中(例如Nor Flash或者RAM),它就不需要加载,可以直接运行;如果代码段位于不能运行的存储区域中(例如:Nand Flash或者硬盘)中,它就必须被加载到RAM运行。

    六、RAM调试运行

    在嵌入式系统中,这是一种常用的调试方式,而不是通常的运行方式。在通常的运行方式下,程序运行的起始地址一般不可能是RAM。RAM在掉电之后内容会丢失,因此系统上电的时候,RAM中一般不会有有效的程序。但是在程序的调试阶段,可以将程序直接载入RAM,然后在RAM的程序载入地址处运行程序。

    这是一种相对简单的形式,因为代码段的存储地址和运行地址是相同的,都是RAM(SDRAM或者SRAM)中的地址。在这种情况下,程序没有运行初始化阶段加载的问题。

    从主机向目标机载入程序的时候,程序映像文件中代码段(code或text)、只读数据段、读写数据段依次载入目标系统RAM(SDRAM或者SRAM)的空间中。

    程序载入到目标机之后,将从代码区的地址开始运行,在运行的初始化阶段,将开辟未初始化数据区,并将其初始化为0,在运行时将动态开辟堆区和栈区。

    在没有操作系统的情况下,开辟内存的工作都是由编译器生成的代码完成的,实现的原理是在映像文件中加入这些代码。主要工作包括:在程序运行时根据实际大小开辟未初始化的数据段;初始化栈区的指针,这个指针和物理内存的实际大小有关;在调用相关函数(malloc、free)时使用堆区,这些函数一般由调用库函数实现。

    知识点:程序直接载入RAM运行时,程序的加载位置和运行位置是一致的,因此不存在段复制的问题,需要在初始化阶段开辟未初始化区域,在运行时使用堆栈。

    七、固化程序的加载运行

    在某些时候,在存放程序的位置是不能运行程序的,例如程序存储在不能以XIP方式运行的Nand-Flash或者硬盘中,在这种情况下,必须将程序完全加载到RAM中才可以运行。

    依照这种方式运行程序,需要将Flash中所有的内容全部复制到SDRAM或者SRAM中。在一般情况下,SDRAM或者SRAM的速度要快于Flash。这样做的另外一个好处是可以加快程序的运行速度。也就是说,即使Flash可以运行程序,将程序加载到RAM中运行也还有一定的优势。

    这样做也产生了另外一个问题:代码段的载入地址和运行地址是不相同的,载入地址是在ROM(Flash)中,但是运行的地址是在RAM(SDRAM或者SRAM)中。对于这个问题,不同的系统在加载程序的时候有不同的解决方式。

    知识点:固化程序在加载运行时,需要复制代码段、只读数据段和读写数据段到RAM中,并另辟未初始化数据段,然后在RAM中运行程序(执行代码段)。

    以这种加载方式的运行程序,另外一个重要的问题是:如何把代码移到RAM中。在有操作系统的情况下,代码的复制工作是由操作系统完成的,在没有操作系统的情况下,处理方式相对复杂,程序需要自我复制。显然,这种方式实现的前提是代码最初放置在可以以XIP方式执行的内存中。

    程序本身复制的过程也是需要通过程序代码完成的,这时需要程序中的代码根据将包含自己的程序从ROM或者Flash中复制到RAM中。这是一个比较复杂的过程,程序的最前面部分是具有复制功能的代码。系统上电后,从ROM或者Flash起始地址运行,具有复制功能的代码将全部代码段和其他需要复制的部分复制到RAM中,然后跳转到RAM中重新运行程序。

    八、固化程序的XIP运行

    固化应用是一种嵌入式系统常用的运行方式,其前提是目标代码位于目标系统ROM(Flash)中。ROM中的区域包括映像文件的代码段(code或text)、只读数据段(RO Data)、读写数据段(RW Data)。

    代码的运行也是在ROM(Flash)中,因此,在编译过程中代码的存储地址和运行地址是相同的,由于上电时需要启动,因此该代码的位置一般是(0x0)。

    在这种应用中,一件重要的事情就是将已初始化读写段的数据从Flash中复制到SDRAM中,由于已初始化读写段既需要固化,也需要在运行时修改,因此这一步是必须有的,在程序的初始化阶段需要完成这一步。

    一般来说,在编译过程中需要定义读写段和未初始化段的地址。在程序中可获取这些地址,然后就可以在程序的中加入复制的代码,实现读写段的转移。

    知识点:程序在ROM或者Flash中以XIP形式运行的时候,不需要复制代码段和只读数据段,但是需要在RAM中复制读写数据段,并另辟未初始化数据段。

    展开全文
  • 程序运行时的内存空间分布

    万次阅读 多人点赞 2014-03-21 01:33:18
    其实在程序运行时,由于内存的管理方式是以页为单位的,而且程序使用的地址都是虚拟地址,当程序要使用内存时,操作系统再把虚拟地址映射到真实的物理内存的地址上。所以在程序中,以虚拟地址来看,数据或代码是一块...
  • 代码段与程序段的区别

    千次阅读 2012-09-21 23:23:18
    一个程序本质上都是由 bss、data、text三个组成的。这样的概念,不知道最初来源于哪里的规定,但 在当前的计算机程序设计中是很重要的一个基本概念。而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统...
  • 本文主要分析char型数值的错误取值导致程序陷入死循环的深层次原因。通过构造一个典型程序死循环问题,跟随本文思维方式,可以让读者从编译器角度深入理解错误原因,还能了解到编译器是如何对char型取值范围进行限制...
  • 程序编译后运行时的内存分配

    千次阅读 2015-07-22 10:00:13
    一、编译时与运行时的内存情况 ...此时只是根据声明时的类型进行占位,到以后程序执行时分配内存才会正确。所以声明是给编译器看的,聪明的编译器能根据声明帮你识别错误。 2.运行时必分配内存 运行
  • C++程序如何编译运行

    千次阅读 2019-06-09 12:27:33
    下面是C++入门的hello world程序源码 #include <iostream> using namespace std; int main(int argc, char* argv[]) { cout << "hello world" << endl; return 0; } 你可以在将上述源码复制...
  • 从编写源代码到程序在内存中运行的全过程解析

    万次阅读 多人点赞 2018-04-21 17:49:51
    作为一个C/C++程序员,搞清楚从编写源代码到程序运行过程中发生的细节是很有必要的。这在之前也是困扰我的一个很大问题,因为最近在忙着找实习,一直没有下定决心来写这篇博客,最近才抽时间写。下面的代码除了明显...
  • 关于程序运行时的数据结构

    千次阅读 2016-04-29 10:54:35
    对于可执行文件,它包含...可得知,程序代码是存放在文本,初始化的全局变量和静态变量是存放到数据中的,而没有初始化的全局变量和静态变量则是存放到BBS,运行时所需要的BSS的大小记录在目标文件中,但BSS
  • 已知字母A的ASCII码为十进制的65,下面程序的输出是______A_____. #include<stdio.h> void main() {char ch1,ch2; ch1='A'+'5'-'3'; ch2='A'+'6'-'3'; printf("%d,%c\n",ch1,ch2); } A) 67,D B) B,C C) C,D D) 不...
  • 案例1: #include using namespace std;... // char* a 与 char a[] 的区别 char* a = "abcdef"; // a为一指针,其值可以改变。现在a指向的是一常量字符串 cout ; a = "ghijkl"; // a现在指向另一常量
  • 根据ANSI C标准的赋值约束条件: 1. 两个操作数都是指向有限定符或无限定符的相容类型的指针。... const char*的类型是:“指向一个具有const限定符的char类型的指针”。(不能修改其值) char*...
  • 测试程序运行时间

    万次阅读 2011-06-14 15:26:00
    测试程序运行时间-time.h1.计时 C/C++中的计时函数是clock(),而与其相关的数据类型是clock_t。在MSDN中,查得对clock函数定义如下: clock_t clock( void ); 这个函数返回从“开启这个程序进程”到“程序中调用...
  • rdtsc指令,测量程序运行速度

    千次阅读 2017-07-19 13:43:56
    最近发现了rdtsc指令,可以获取CPU指令周期数,喜出望外,wiki了下相关的知识,写了代码利用CPU周期来测量程序运行时间。    rdtsc指令返回的是自开机始CPU的周期数,返回的是一个64位的值EDX:EAX(高32...
  • linux 统计 程序运行时间

    万次阅读 2011-08-29 16:48:33
    我们有时需要得到程序运行时间,但我们也要知道,根本不可能精确测量某一个程序运行的确切时间­[3],文献[4]中说的很明白,现摘录如下。  我们平时常用的测量运行时间的方法并不是那么精确的,换句话说,想精确...
  • 产生错误就是访问了错误的内存,一般是你没有...一般来说,错误就是指访问的内存超出了系统所给这个程序的内存空间,通常这个值是由gdtr来保存的,它是一个48位的寄存器,其中的32位是保 存由它指向的gdt表,后
  • c++记录程序运行时间

    万次阅读 2014-04-02 15:01:40
    DWORD start_time=GetTickCount(); {...} DWORD end_time=GetTickCount(); ... DWORD Subtime = (end_time-start_time);...如何获取代码运行时间 ...在调试中,经常需要计算某一代码的执行时间,下面给出两种常用
  • 在一个系统刚开始时,并没有什么资源可以使用,CPU只认得0x00000000地址,并从那里运行第一条指令,并且这代码有大小限制,不可以很大。因此需要开发一个引导程序放在那里运行,在这里的培训课程里,主要使用是S3C...
  • 深入理解程序从编译到运行

    万次阅读 多人点赞 2017-10-17 10:47:18
    From:... 从Hello World说程序运行机制:http://www.sohu.com/a/132798003_505868 C/C++中如何在main()函数之前执行一条语句?:https://www.zhihu.com/question/26031933 (深入理解计算机系统...
  • 使用gdb调试当前运行程序

    万次阅读 2010-09-17 11:16:00
    用gdb可以调试当前的程序的使用情况,读出他的参数。以下用一个简单的程序做为例子:来说明gdb的调试。第一步 编译一个死循环程序。/* File name malloc.c*/#include #include #include void getmem(void **p, int ...
  • 计算程序运行时间

    千次阅读 2013-09-05 15:06:39
     具体使用,就是在要计算运行时间的程序段之前和之后分别加上gettimeofday( &tvstart, NULL)、gettimeofday(&tvend, NULL),然后计算: (tvend.tv_sec-tvstart.tv_sec)+(tvend.tv_usec-tvstart.tv_usec)/1000000...
  • strcpy,strncpy,char*s ,char s[]

    千次阅读 2013-10-13 22:21:07
    [原]程序员笔试题---说说char *s 和char s[] 一、 关于char *s 和char s[]区别的笔试题,虽说占的分数不多,但是基本每家公司都会考!所以在此做个小总结!避免自己在犯错误!当初就是因为这么小的...
  • 程序运行过程中内存的分配问题

    千次阅读 2016-05-04 15:30:29
    第1节主要介绍内存管理基本概念,重点介绍C程序中内存的分配,以及C语言编译后的可执行程序的存储结构和运行结构,同时还介绍了堆空间和栈空间的用途及区别。 第2节主要介绍C语言中内存分配及释放函数、函数的功能...
  • 使用Keil MDK运行第一个STM32程序

    千次阅读 2018-08-06 12:19:33
    这里选择较为相近的Interrupt文件夹下得例程,打开文件夹下面的mian.c文件,通过简单的浏览可以找到如下一段程序: /* USARTy and USARTz configuration ----------------------------------------------------...
  • linux 计算程序运行时间

    千次阅读 2011-07-19 11:59:01
    计算程序运行时间 (2009-03-30 12:44) 分类: c 写的全面的一篇还有一种int getitimer(int which, struct itimerval *value);int setitimer(int which, const struct itimerv

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 198,652
精华内容 79,460
关键字:

下面程序段的运行结果是char