Linux操作系统实验报告单(8)Linux开发工具的使用

一、实验目的

本实验旨在通过实践掌握GCC编译器的基础用法和GDB调试器的核心调试技巧,具体包含以下核心能力培养:

  1. GCC编译流程控制:从预处理到链接的全流程控制,掌握多文件编译和优化选项应用
  2. GDB调试方法论:包括断点管理、变量监控、堆栈跟踪等系统化调试方法

二、实验内容

(1)编译器gcc的使用

1)编辑一个C语言程序文件 hello.c ,代码如下:

#include <stdio.h>

main()

{

char name[20];

printf(“Please input your name:”);

scanf(“%s”,name);

printf(“Welcome %s!\n”,name);

return 0;

}

    2)编译文件: gcc -o hello hello.c。

    3)若有错误,修改hello.c的内容,然后再次编译,直至没有错误为止。

    实验步骤:
    ①编写代码文件:

    ②第一次运行报错如下:

    主要问题点:

    1. 中文标点符号:代码中双引号为中文全角字符(“”),应替换为英文半角("")
    2. main函数声明不规范:未明确返回值类型,建议改为int main(void)
    3. 缓冲区溢出风险:scanf未限制输入长度,建议使用scanf("%19s", name)
    ③修改代码如下:
    #include <stdio.h>
    
    int main(void)
    
    {
    
        char name[20];
    
        printf("Please input your name: ");
    
        scanf("%19s", name);
    
        printf("Welcome %s!\n", name);
    
        return 0;
    
    }
    ④再次运行gcc -o hello hello.c,无报错,结果如下:

    ⑤编译命令演进:
    # 基础编译
    gcc -o hello hello.c
    
    # 开启所有警告(推荐)
    gcc -Wall -Wextra -o hello hello.c
    
    # 分阶段编译演示
    gcc -E hello.c -o hello.i  # 预处理
    gcc -S hello.i -o hello.s  # 汇编代码生成
    gcc -c hello.s -o hello.o  # 目标文件生成
    gcc hello.o -o hello       # 链接
    

    (2)使用GDB 调试程序BUG

    1)使用文本编辑器输入以下代码greet.c。程序试图倒序输出main 函数中定义的字符串,但结果

      2)使用gcc –g 的选项编译这段代码,运行生成的可执行文件,观察运行结果。

      3)使用gdb 调试程序,通过设置断点、单步跟踪,一步步找出错误所在。(调试过程需截图)

      4)纠正错误,更改源程序并得到正确的结果。

      实验步骤:
      ①编写代码文件:

      ②编译带调试信息的可执行文件:

      输入命令:gcc -g -Wall -o greet greet.c,发现报错如下:

      验证调试信息是否存在:readelf -S greet | grep debug  # 应显示.debug_info等段,结果如下:

      ③gdb 调试:
      Step 1: 验证编译(上面得到的警告提示):
      greet.c: 在函数‘display2’中:
      greet.c:25:5: 警告:隐式声明函数‘strlen’ [-Wimplicit-function-declaration]
         25 | size = strlen(string1);
            |     ^\~~\~~\~
      greet.c:28:5: 警告:隐式声明函数‘malloc’ [-Wimplicit-function-declaration]
         28 | string2 = (char *)malloc(size + 1);
            |     ^\~~\~~\~
      

      Step 2: 启动GDB调试器
      gdb greet
      

      Step 3: 设置关键断点
      # 在display2函数入口设置断点
      (gdb) break display2
      Breakpoint 1 at 0x11e5: file greet.c, line 15.
      
      # 在可能出错的行设置断点
      (gdb) break 18   # size = strlen(string1);
      (gdb) break 22   # string2[size-i] = string1[i];
      (gdb) break 24   # string2[size+1]='\0';
      

      查看断点

      (gdb) info breakpoints
      Num     Type           Disp Enb Address            What
      1       breakpoint     keep y   0x00000000000011e5 in display2 at greet.c:15
      2       breakpoint     keep y   0x00000000000011f1 in display2 at greet.c:18
      3       breakpoint     keep y   0x0000000000001234 in display2 at greet.c:22
      4       breakpoint     keep y   0x000000000000125d in display2 at greet.c:24
      

      运行结果如图:


      Step 4: 运行程序

      在命令行中输入:run,查看运行结果:


      Step 5: 单步调试关键代码

      执行到size = strlen(string1)

      (gdb) next
      18      size = strlen(string1);
      (gdb) print string1
      $1 = 0x7fffffffde70 "Welcome to Linux!"
      

      查看字符串长度

      (gdb) next
      19      string2 = (char *)malloc(size + 1);
      (gdb) print size
      $2 = 17  # "Welcome to Linux!"长度为17
      

      运行结果如图:


      Step 6: 检查内存分配
      (gdb) next
      20      for(i=0;i<size;i++)
      (gdb) print string2
      $3 = 0x5555555596b0 ""  # 分配17字节(0x11)
      (gdb) x/17xb string2  # 查看17字节内存(全为0)
      0x5555555596b0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
      0x5555555596b8: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
      0x5555555596c0: 0x00
      

      运行结果如图:


      Step 7: 跟踪循环写入

      执行第一次循环

      (gdb) next
      22          string2[size-i] = string1[i];
      (gdb) print i
      $4 = 0
      (gdb) print size-i
      $5 = 17  # 写入位置string2[17](越界!分配空间索引0-17)
      

      查看内存变化

      (gdb) next
      28      for(i=0;i<size;i++)
      (gdb) x/17xb string2
      0x5555555596b0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
      0x5555555596b8: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x57 ('W')
      0x5555555596c0: 0x00  # string2[16]='W'(实际有效索引0-16)
      

      Step 8: 发现终止符错误

      执行到终止符赋值

      (gdb) continue
      Continuing.
      
      Breakpoint 3, display2 (string1=0x7fffffffde70 "Welcome to Linux!") at greet.c:22
      22      string2[size+1]='\0';
      (gdb) print size+1
      $6 = 18  # 尝试写入string2[18](越界!)
      

      内存状态检查

      (gdb) x/17xb string2
      0x5555555596b0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
      0x5555555596b8: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x57 ('W')
      0x5555555596c0: 0x65 ('e')  # 最后一个有效位置是16(0x6c0)
      

      错误总结

      1.索引越界

      string2[size-i] = string1[i];  // 应改为 string2[size-i-1]
      
      • 当i=0时,写入位置为size(超出0~size-1范围)
      • 导致字符串起始位置错误(从索引1开始)

      2.终止符错误

      string2[size+1] = '\0';  // 应改为 string2[size] = '\0'
      
      • 写入位置超出分配空间(分配size+1字节,有效索引0~size)

      ④修正后代码(display2函数):
      #include <stdio.h>
      #include <string.h>  // 添加头文件
      #include <stdlib.h>  // 添加头文件
      
      int display2(char *string1) 
      {
          char *string2;
          int size = strlen(string1);
          string2 = (char *)malloc(size + 1);
          
          for(int i=0; i<size; i++) {
              string2[size-i-1] = string1[i];  // 修正索引
          }
          string2[size] = '\0';  // 修正终止符
          
          printf("The string afterward is %s\n", string2);
          free(string2);  // 释放内存
          return 0;
      }
      

      完整代码如图:


      ⑤验证修改后的输出结果:
      $ ./greet
      The original string is Welcome to Linux! 
      The string afterward is !xuniL ot emocleW

      (3)关于GDB调试

      GDB核心命令速查表
      命令功能描述
      gdb ./greet启动调试器
      break display2在函数入口设断点
      run启动程序运行
      next (n)单步执行(不进入函数)
      step (s)单步进入函数
      print variable打印变量值
      backtrace (bt)显示调用堆栈
      info breakpoints查看所有断点
      x/10xb string2检查内存内容(16进制显示10字节)
      典型调试流程
      1. 设置观察点

        (gdb) break display2
        (gdb) run
        
      2. 变量监控

        (gdb) watch string2[size]  # 监控关键内存位置
        
      3. 内存诊断

        (gdb) print sizeof(string1)
        (gdb) x/16xb string2  # 十六进制查看内存
        
      4. 错误定位
        当程序在string2[size+1] = '\0'处崩溃时,使用backtrace查看调用栈,frame N切换堆栈帧检查局部变量

      关键概念解析

      GCC调试选项原理

      • -g选项生成DWARF格式调试信息,包含:
      • 源代码行号映射
      • 变量类型信息
      • 函数调用关系
      • 调试信息级别:
      选项信息量适用场景
      -g0无调试信息发布版本
      -g1最小信息(函数名、外部变量)崩溃报告
      -g2默认级别(含行号、局部变量)常规调试
      -g3含宏定义等扩展信息复杂调试场景

      字符串反转算法优化
      正确实现方式

      void reverseString(char *str) {
          int len = strlen(str);
          for(int i=0; i<len/2; i++) {
              char temp = str[i];
              str[i] = str[len-i-1];
              str[len-i-1] = temp;
          }
      }
      

      该实现:

      • 时间复杂度O(n/2)
      • 空间复杂度O(1)
      • 原地操作节省内存

      内存管理规范

      • malloc分配策略:

        char *buf = malloc(strlen(src)+1);
        if(!buf) { /* 错误处理 */ }
        strcpy(buf, src);
        /* 使用后 */
        free(buf);
        
      • 常见错误模式:

      错误类型后果检测方法
      缓冲区溢出内存损坏valgrind工具检测
      使用未初始化指针段错误编译警告-Wuninitialized
      内存泄漏资源耗尽valgrind --leak-check

      通过本实验应掌握以下工程实践能力:

      1. 防御性编程技巧

        • 输入验证:限制scanf输入长度
        • 错误处理:检查malloc返回值
        • 资源管理:成对的malloc/free操作
      2. 调试方法论

        • 二分法定位:通过设置中间断点快速缩小问题范围
        • 最小复现样例:将问题代码从复杂系统中剥离测试
        • 回归测试:修正错误后验证其他功能不受影响
      3. 质量保障体系

      建议后续扩展:

      • 结合Makefile实现自动化构建
      • 使用Valgrind进行内存泄漏检测
      • 配置VS Code远程调试环境提升效率

      三、实验总结

              在完成这次实验的过程中,我深刻体会到编程不仅是代码的书写,更是一场与细节的博弈。最初面对满屏的警告和运行时的沉默输出,那种无从下手的焦虑感至今记忆犹新。当GDB第一次在断点处停下时,看着内存中错位的字符数据,才真正理解到计算机不会说谎——每一个偏移的计算错误、每一处越界的赋值操作,都在冰冷的寄存器中暴露无遗。

              调试过程中最让我震撼的是内存空间的具象化呈现。通过x命令查看内存块时,那些原本抽象的数组索引突然变得触手可及。当亲眼看到string2[16]的位置被错误地写入首字母'W'时,瞬间明白了教科书上强调的"数组从0开始"不是教条,而是真实的物理存储规律。这种从理论到现实的映射,比任何课堂讲解都更有说服力。

              在反复的单步执行中,我逐渐领悟到调试器像是程序员的时空机器。通过回看函数调用栈,能清晰看到程序崩溃前的行为轨迹;设置条件断点时,仿佛获得了在时间线上自由穿梭的能力。这种对程序运行状态的完全掌控,让我对"程序是可控的"这句话有了全新认知——即便面对看似随机的崩溃,只要方法得当,总能找到确定的因果链。

              这次实验也让我意识到,优秀的代码往往诞生于与编译器的持续对话中。那些起初令人烦躁的-Wall警告,实际上是最严格的语法老师。当开始主动根据警告信息修正代码风格时,发现很多潜在的错误在编译阶段就被提前拦截。这种预防性编程的思维,远比事后调试更有价值。

              看着最终正确输出的倒序字符串,不仅是一个实验的完成,更是思维模式的升级。现在的我,开始习惯在写每个循环时下意识检查边界,在每次内存分配后条件反射地思考释放点。这些刻入潜意识的编程习惯,或许就是这次实验给予我最宝贵的礼物。当工具的使用升华成本能,才能真正称得上与机器达成了默契。

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包

      打赏作者

      小李独爱秋

      你的鼓励将是我加更的最大动力

      ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
      扫码支付:¥1
      获取中
      扫码支付

      您的余额不足,请更换扫码支付或充值

      打赏作者

      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

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

      余额充值