• 计算机基础(三):C语言与汇编


    编程语言的发展

    上一篇计算机基础(二):汇编语言与内存介绍了汇编语言,却没有给出汇编代码,主要是因为汇编虽然简单、直接,但人们很少使用汇编直接编写程序。

    汇编虽然不再要求我们写0、1组合,只需要对着CPU厂商的开发手册写程序就行,但是要我们熟悉CPU的构造,以及计算机组成原理、体系结构等等知识,其实我们只想关注要通过计算机解决的计算问题,使用、熟悉计算机并不是目的,这些只是手段。实现程序时思维需要在解决的问题与计算机底层知识之间相互切换,使用汇编的成本还是太高。因此编程语言更进一步,继续向前发展,抽象出了C语言,继而又产生了其他更多的高级语言。

    语言的设计

    编程语言到底干了一件什么事儿?一言以蔽之:操作数据。通过操作数据,实现通过有限次的运算步骤,得到最终结算问题的解。具体这些操作包括如下:

    1. 移动:从内存到CPU、从CPU到内存;
    2. 基本运算: +、-、*、/ 等基本数学运算;
    3. 逻辑运算:移位、与、或、非等逻辑运算。

    那如何设计一门语言,当然也应该围绕着操作数据而展开。另外,每一门语言都是为了解决它上一级语言的问题,也就是对它上一级语言的高度抽象,比如汇编语言是对ISA指令集的抽象,C语言是对汇编语言的高度抽象。但无论如何,设计一门语言始终包含如下三个方面:

    1. 数据类型系统;
    2. 抽象对数据的操作;
    3. 添加语言自身的特性。

    C语言的设计

    C语言使用int、long定义数据类型,使用 +、-、*、/ 等符号抽象汇编中的运算指令操作,而对于语言自身的特性而言,C语言并没有其自身的特性,它是对汇编语言纯粹的抽象,具体表现为:

    1. 指令段:抽象 为 函数;
    2. 操作单元的大小 抽象为类型系统(基础数据类型-byte、short、int,内存单元2^n(n>=0)),其他类型:结构体(表示不规则内存单元)、数组(相同类型的多个内存单元));
    3. 指令段之间的调用抽象为函数之间的调用4. 数据地址操作抽象为指针操作

    C语言与汇编的映射

    函数调用

    先来看一段C语言的HelloWorld:

    #include 
      
    int main()
    {
        /* 我的第一个 C 程序 */
        printf("Hello, World! \n");
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后编译为汇编代码:

    gcc -S  -fno-asynchronous-unwind-tables helloc.c
    
    cat helloc.s 
    
    • 1
    • 2
    • 3

    具体展示汇编代码如下:

            .file   "helloc.c"
            .text
            .section        .rodata
    .LC0:
            .string "Hello, World! "
            .text
            .globl  main
            .type   main, @function
    main:
            endbr64
            pushq   %rbp
            movq    %rsp, %rbp
            leaq    .LC0(%rip), %rdi
            call    puts@PLT
            movl    $0, %eax
            popq    %rbp
            ret
            .size   main, .-main
            .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
            .section        .note.GNU-stack,"",@progbits
            .section        .note.gnu.property,"a"
            .align 8
            .long    1f - 0f
            .long    4f - 1f
            .long    5
    0:
            .string  "GNU"
    1:
            .align 8
            .long    0xc0000002
            .long    3f - 2f
    2:
            .long    0x3
    3:
            .align 8
    4:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    1. 大致看一下main指令片段中的含义,首先是开辟当前指令片段的栈桢,rbp寄存器指向栈底,rsp指向栈顶:
      pushq %rbp // 将上一个指令片段的栈底地址压栈
      movq %rsp, %rbp // 将当前栈顶地址覆盖给栈底寄存器

    2. leaq 取出 LCO 地址,赋值给rdi寄存器,然后 call 调用另一个指令片段,我们有理由猜测其实际上是对应的调用 printf 来实现打印的功能,上一步可能就是传参的动作。
      leaq .LC0(%rip), %rdi
      call puts@PLT

    3. 将 0 这个数放到 eax 寄存器,弹出rbp,即当前main的指令片段的栈底寄存器,ret退出执行,也就表示了main的执行结束,同时eax寄存器中存储着函数返回值。
      movl $0, %eax
      popq %rbp
      ret

    小结

    联系到上一篇的内容,这次我们从实际的汇编代码层面理解了函数调用的压栈与出栈操作。

  • 相关阅读:
    2021-06-15 51单片机c语言秒表的仿真ISIS7 professional
    Pinia与Vuex使用区别
    物理机服务器应该注意的事
    C/C++刷题DAY2
    LangChain-v0.2 构建聊天机器人
    SCHP(CVPR2019)-人体解析论文阅读
    【全】【ES集群安装+配置教程】装ElasticSearch到CentOS 8中liunx
    Linux进程间通信之匿名管道
    影刀RPA在web中表格类型数据的处理
    CPU受限直接执行
  • 原文地址:https://blog.csdn.net/lyg673770712/article/details/126780951