带你学习《深入理解计算机系统》程序语言的底层描述(3)——调用者保存与内存地址跳转

本文深入解析了程序语言底层的调用者保存与内存地址跳转机制。通过示例代码,解释了编译器如何使用寄存器,特别是在函数调用中`eax`、`ebx`的保存规则。`eax`作为“调用者保存”,需要父函数在调用子函数前备份,而`ebx`作为“被调用者保存”,由子函数负责保存。此外,文章还探讨了程序计数器在跳转过程中的作用,以及动态链接与静态链接在调用函数时的差异。

先做个更正,之前提到的代码地址,其实是汇编代码的首位数字,正规名应该是程序计数器才对O(∩_∩)O~

一:编译器对寄存器的使用规则

在第(2)节的最后,我们讨论了最简单的函数调用ebpesp.c的汇编实现,这里我们做一个小改,增加一句打印sum的操作:

int son_add(int a, int b)
{
        return a+b;
}

int father()
{
        int a = 1;
        int b = 2;
        int sum = 0;
        sum = son_add(a, b);

printf("%d\n", sum);
        return sum;
}

00000000 <son_add>:
   0:   55                      push   %ebp
   1:   89 e5                 mov    %esp,%ebp
   3:   8b 45 0c            mov    0xc(%ebp),%eax
   6:   03 45 08            add    0x8(%ebp),%eax
   9:   c9                      leave  
   a:   c3                      ret    
   b:   90                      nop    


0000000c <father>:
   c:   55                        push   %ebp
   d:   89 e5                   mov    %esp,%ebp
   f:   53                         push   %ebx
  10:   50                       push   %eax
  11:   6a 02                  push   $0x9
  13:   6a 01                  push   $0x8
  15:   e8 fc ff ff ff          call   16 <father+0xa>//调用son_add
  1a:   50                       push   %eax
  1b:   89 c3                  mov    %eax,%ebx
  1d:   68 00 00 00 00   push   $0x0
  22:   e8 fc ff ff ff          call   23 <father+0x17>//调用printf
  27:   89 d8                  mov    %ebx,%eax
  29:   8b 5d fc              mov    0xfffffffc(%ebp),%ebx
  2c:   c9                       leave  
  2d:   c3                       ret  

 

有几个遗留问题未说明,比如,son_add()在计算出sum的和之后,通过什么方式返回给father?千万别盯着ret看,在(2)节已经写出我YY的ret等价汇编伪代码,里面丝毫没提返回值的事。

仔细观察son_add函数程序计数器6的操作,计算的和被赋值给寄存器%eax,GCC默认用%eax来

### 关于《深入理解计算机系统》第八章实验的内容 #### 实验概述 《深入理解计算机系统》第八章主要围绕操作系统的核心概念展开,重点讨论了进程管理、内存管理文件系统的实现机制。本章节的实验通常涉及以下几个方面: 1. **进程创建控制** 使用 `fork()` `execve()` 函数来创建子进程并加载新的可执行程序[^1]。 2. **异常处理系统调用** 学习如何通过系统调用触发异常,并了解其分类(中断、陷阱、故障终止)。特别是陷阱的作用在于提供用户空间程序内核之间的交互接口[^2]。 3. **进程标识符操作** 掌握获取当前进程 ID (`getpid()`) 及其父进程 ID (`getppid()`) 的方法[^3]。 --- #### 示例代码解析 以下是基于第八章内容的一个典型实验案例——使用 `execve` 加载外部命令 `/bin/ls` 并打印错误信息: ```c #include "csapp.h" int main(int argc, const char *argv[], const char *envp[]) { if (execve("/bin/ls", argv, envp) < 0) { // 尝试加载 /bin/ls 命令 printf("/bin/ls not found.\n"); return -1; } return 0; } ``` 此代码片段展示了如何利用 `execve` 替换当前进程映像为指定路径下的新程序。如果目标程序不存在,则会进入条件分支输出提示信息[^1]。 --- #### 系统调用异常处理详解 在现代操作系统中,异常是一种重要的控制流转移方式。当处理器检测到特定事件时,它会依据预设的异常向量表跳转至对应处理函数。例如,在用户态发起一条 `syscall` 指令即可触发展现形式之一——陷阱[^2]。 这种设计允许应用程序安全地请求服务而无需直接访问硬件资源。常见的场景包括但不限于打开文件、分配内存以及网络通信等基础功能支持。 --- #### 获取进程ID的实际应用 为了更好地理解调试多线程或多进程环境下的行为模式,《深入理解计算机系统》还介绍了两个关键 API —— `getpid()` `getppid()` 。它们分别用于查询当前运行实例及其上级启动者的唯一编号。 下面给出一段简单的演示代码: ```c #include <stdio.h> #include <unistd.h> int main() { pid_t current_pid = getpid(); // 调用 getpid() pid_t parent_pid = getppid(); // 调用 getppid() printf("Current Process ID: %d\nParent Process ID: %d\n", current_pid, parent_pid); return 0; } ``` 以上例子清晰表明了父子关系链路结构对于构建复杂软件架构的重要性。 --- ### 总结 通过对上述知识点的学习可以加深读者对底层原理的认识程度;同时也能培养动手实践能力从而达到理论联系实际的目的。希望这些材料能够帮助您顺利完成课程安排的任务! ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值