1 重新搭建实验环境

前面都是用实验楼环境做的实验,偷的懒总是要还的,这一次重装环境前后花了十几个小时,踩了无数的坑。

1.1 Ubuntu和LINUX内核的区别

Ubuntu是基于LINUX内核编写的一个操作系统。LINUX内核定义了一些基本的系统功能,Ubuntu在内核之上加入了图形界面,包管理等功能,优化了人机交互。本次实验,要求使用LINUX内核5.0以上,所以,在下载安装完Ubuntu系统后,需要对内核进行更新。

$ uname -a

上面这个指令会显示Ubuntu当前的内核版本,我们可以通过它来观察内核的升级是否成功。

1.2 从零开始

下载安装Ubuntu

首先到Ubuntu官网上下载一个Ubuntu镜像,但是太慢了,我们可以在国内的镜像网站上去下载。指路网易镜像

下载完成后,在VMware虚拟机中进行系统安装,没什么可说的。

  • 设置超级管理员

    新装的系统没有超级管理员,所以需要先设置一个。执行下面的命令,按照提示要求完成管理员注册。
sudo passwd root
  • 设置共享文件夹

    为了方便VMware内虚拟主机和我们的主机进行交互,可以设置一个共享文件夹

    首先将虚拟主机关机,然后在虚拟机设置=》选项卡中设置共享文件夹。安装VMwareTools,在VMware菜单栏,点击“重新安装VMwareTools”。虚拟主机内会出现资源管理器,里面有下载好的压缩包,将它拷贝到桌面上解压。然后执行VMware底部弹出的建议命令,完成安装。查看共享文件夹。共享文件夹的位置在/mnt/hfgs/share/

更换国内源

国外的资源下载速度实在太慢,所以在开始工作之前,建议先更换成国内镜像,指路科大镜像

  • 备份原始源
$ sudo cp /etc/apt/sources.list /etc/apt/sources_backup.list
  • 修改配置文件
$ sudo gedit /etc/apt/sources.list

把从网上找到的资源列表复制拷贝过来,点击资源管理器右上角的save按钮

  • 更新源
$ sudo apt-get update

下载编译LINUX5.0内核

先下载5.0以上linux内核。

  • 解压内核文件
$ xz -d linux-5.0.1.tar.xz
$ tar -xvf linux-5.0.1.tar
  • 安装依赖
$ sudo apt-get install build-essential
$ sudo apt-get install libelf-dev
$ sudo apt-get install libncurses-dev
$ sudo apt-get install flex
$ sudo apt-get install bison
$ sudo apt-get install libssl-dev
  • 配置内核
$ cd /linux/5.0.1
$ sudo cp /boot/config-5.0.23-generic -r .config
$ sudo make oldconfig
$ sudo make localmodconfig
$ make menuconfig

在弹出的图形化界面中配置

kernel hacking -> compile-time and compiler options 勾选 [*] compiler the kernel with debug info

  • 编译内核
$ sudo make
$ sudo make modules_install
# 更新
$ sudo make install
  • 重启虚拟机

    查看内核版本是否已经是5.0.1
$ uname -a

1.3 搭建实验环境

  • 安装qemu模拟命令,加载linux内核
$ sudo apt install qemu
$ qemu-sysem-x86_64 -kernel linux-5.0.1/arch/x86_64/boot/bzIamge
  • 剩余的部分主要是配置qemu环境,把写好的replyhi网络聊天程序集成到qemu中,和上一次实验内容相同,不再重复演示。

2 Socket系统调用分析

按照实验要求,我们分为两个方向来研究Socket系统调用。实验指出,内核将系统调用作为一个特殊中断来处理,因此首先我们对这一点进行验证;其次我们将探究,对于不同的协议,Socket系统调用源码中是如何封装协议细节的,是否使用了实验提到的“多态”机制,怎么实现的。

2.1 系统调用的中断实现

修改Makefile

为探究64位程序中socket的系统调用行为,我们首先需要对上一节使用到的Makefile进行修改

#
# Makefile for linuxnet/lab3
#
# ... 省略前文 rootfs:
gcc -o init linktable.c menu.c main.c -m64 -static -lpthread
find init | cpio -o -Hnewc |gzip -9 > ../../rootfs.img
qemu-system-x86_64 -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../../rootfs.img -append nokaslr -s -S # ...省略后文

在编译指令gcc那一行,将编译选项由-m32改为-m64

执行指令

$ make rootfs

我们得到了新的64位可执行文件init

GDB调试

使用GDB调试init,在socket函数前打上断点。

$ gdb init
$ (gdb) break socket

打开汇编窗口,查看代码运行情况

$ (gdb) layout asm

可以看到,程序在socket函数入口处停下,下一条汇编指令是一个syscall的系统调用。

反汇编init

init进行反汇编

$ objdump -d init > init_ASM.txt

查看init_ASM.txt文件,在第104553行找到socket对应的系统调用。

证明对于socket api的调用是通过socketcall这个特殊中断来实现的。

syscall的具体实现

利用同样的办法,我们按照上一节的方法启动qemu进行远程调试,设置如下断点:

$ (gdb) break sys_socketcall

跟踪到一个关键函数:SYSCALL_DEFINE2(),它位于linux-5.0.1/net/socket.c之中。

关键代码如下:

switch (call) {
case SYS_SOCKET:
err = __sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = __sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
// ... 省略其余部分
}

可见,每次socket都会调用同一个函数,通过传入的call值不同,在分支语句中执行对应的系统服务例程。

2.2 Socket封装网络协议的多态机制

__sys_socket()为例,其源码位于同一文件下,也是C语言实现的:

int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags; /* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval; return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

注意到函数的传入参数中有一个protocol变量,它用来指定传入的协议是多少。对于系统底层来说,不同的protocol值对应不同的协议类型,而对于socket通信来说,它只负责从高层接受这个字段值,然后交付更底层的函数,在这里,调用到的sock_create代码如下:

int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf; /*
* Check protocol is in range
*/
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL; /* Compatibility. This uglymoron is moved from INET layer to here to avoid
deadlock in module load.
*/
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
family = PF_PACKET;
} err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
// 省略后文

可以发现这个函数仍然不是最底层的函数,它根据情况继续调用security_socket_creat(),或者返回协议错误信息。

从代码上来看,Socket封装协议细节,使用到的应该是名为socket的结构体,在__sys_bind()等函数中,协议字段作为地址长度被传入,说明对于socket来说是通过判断协议字段长度来区分ipv4和ipv6两种不同协议的。在socket结构体中,有一个名为sk_family的字段,通过它的取值不同来判断这个socket是使用ipv4还是ipv6。可以从socket.c中的代码印证这一点:

/* This routine returns the IP overhead imposed by a socket i.e.
* the length of the underlying IP header, depending on whether
* this is an IPv4 or IPv6 socket and the length from IP options turned
* on at the socket. Assumes that the caller has a lock on the socket.
*/
u32 kernel_sock_ip_overhead(struct sock *sk)
{
struct inet_sock *inet;
struct ip_options_rcu *opt;
u32 overhead = 0;
#if IS_ENABLED(CONFIG_IPV6)
struct ipv6_pinfo *np;
struct ipv6_txoptions *optv6 = NULL;
#endif /* IS_ENABLED(CONFIG_IPV6) */ if (!sk)
return overhead; switch (sk->sk_family) {
case AF_INET:
inet = inet_sk(sk);
overhead += sizeof(struct iphdr);
opt = rcu_dereference_protected(inet->inet_opt,
sock_owned_by_user(sk));
if (opt)
overhead += opt->opt.optlen;
return overhead;
#if IS_ENABLED(CONFIG_IPV6)
case AF_INET6:
np = inet6_sk(sk);
overhead += sizeof(struct ipv6hdr);
if (np)
optv6 = rcu_dereference_protected(np->opt,
sock_owned_by_user(sk));
if (optv6)
overhead += (optv6->opt_flen + optv6->opt_nflen);
return overhead;
#endif /* IS_ENABLED(CONFIG_IPV6) */
default: /* Returns 0 overhead if the socket is not ipv4 or ipv6 */
return overhead;
}
}
EXPORT_SYMBOL(kernel_sock_ip_overhead);

综上所述,socket实现了协议封装的多态,它通过结构体的形式,用协议字段的长度作为划分协议的依据,以此将ipv4和ipv6区分开来。而对于调用这些函数和api的高层来说,不管自己是什么协议都调用同样的函数。

从零开始—Socket系统调用和多态封装的更多相关文章

  1. python面向对象之继承/多态/封装

    老师说,按继承/多态/封装这个顺序来讲. 子类使用父类的方法: #!/usr/bin/env python # coding:utf-8 class Vehicle: def __init__(sel ...

  2. 组合&多态&封装

    目录 组合&多态&封装 一.组合 1.1什么是组合 1.2 为什么要用组合 1.3 如何使用组合 1.4 继承和组合都在什么时候用 二.多态与多态性 2.1 什么是多态 2.2 如何用 ...

  3. 继承 & 多态 & 封装

    什么是继承 继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类 python中类的继承分为:单继承和多继承 class Par ...

  4. python面向对象 : 抽象类(接口类),多态,封装(私有制封装)

    一. 抽象类(接口类) 与java一样, python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类, 它的特殊之处在于只能被继承, 不能被实例化. 从设计角度去看, 如果类是从现实对 ...

  5. day25类的组合多态封装

    类的组合多态与封装类的组合 1. 什么是组合  组合指的是某一个对象拥有一个属性,该属性的值是另外一个类的对象 2. 为何要用组合  通过为某一个对象添加属性(属性的值是另外一个类的对象)的方式,可以 ...

  6. 【学习笔记】--- 老男孩学Python,day18 面向对象------抽象类(接口类), 多态, 封装

    抽象类,接口类 Python没有接口这个概念 抽象类(接口类): 目的是制定一个规范 要学会归一化设计,有重复的东西就要想把它们合并起来 from abc import ABCMeta, abstra ...

  7. socket系统调用

    SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; in ...

  8. python oop常用术语 继承 多态 封装

    面向对象优点 1.通过封装明确了内外 2.通过继承+多态在语言层面支持了归一化设计 抽象/实现 抽象指对现实世界问题和实体的本质表现,行为和特征建模,建立一个相关的子集,可以用于 绘程序结构,从而实现 ...

  9. python day - 19 抽象类 接口类 多态 封装

    一. 抽象类接口类即制定一个规范 特点: 1.不可被实例化. 2.规范子类当中必须事先某个方法. 3.在python中有原生实现抽象类的方法,但没有原生实现接口类的方法. 例题:制定一个规范就是,子类 ...

随机推荐

  1. 注意android辅助服务事件不能用于保存

    本来希望把来自辅助服务的事件,像epoll那样暂存在队列进行调度,或者做成事件堆栈,从而将辅助服务事件加入到容器.但是一直不能达到预期的后果.最后才发现一个坑人的事实,辅助服务事件被释放(或者说重置) ...

  2. 你真的会用JavaScript中的sort方法吗

      在平时的业务开发中,数组(Array) 是我们经常用到的数据类型,那么对数组的排序也很常见,除去使用循环遍历数组的方法来排列数据,使用JS数组中原生的方法 sort 来排列(没错,比较崇尚JS原生 ...

  3. react路由的动态传参

    ① 定义规则 ②传值 ③获取传过来的值

  4. PostGIS 安装教程(Linux)(一)

    ##本文分两部分,第一部分讲linux下postgresql的安装,第二部分讲postgis的安装 ##感谢作者:https://www.linuxidc.com/Linux/2017-10/1475 ...

  5. Redis 的底层数据结构(对象)

    目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层.最常用的数据结构,相信你也掌握的不错. 但 redis 实际存储键值对的时候,是基于对 ...

  6. Windows Server 2008 服务器重启后卡死在Windows Update 页面问题处理

    Windows Update 服务器 服务器是联想RD640 操作系统Windows Server 2008 R2 Enterprise版 补丁版本是SP1 远程windows服务器时,一直处于远程建 ...

  7. mac 安装zmap

    mac安装zmap有两种方式 1.命令行安装 brew install zmap export PATH=$PATH:/usr/local/sbin 很多人安装的时候只有第一步,然后直接bash:zm ...

  8. windows和linux的开机顺序

    windows的开机顺序: 启动自检阶段---初始化启动阶段---Boot加载阶段---检测和配置硬件阶段---内核加载阶段---屏幕显示. linux的开机启动顺序: 加载Bios---读取MBR- ...

  9. Redis系列(四):Redis持久化和主从复制原理

    一.持久化 所谓的持久化就是把内存中的数据写到磁盘中去,防止服务宕机后内存数据丢失.Redis4.0之前提供了两种持久化方式:RDB(默认) 和AOF,Redis4.x之后新增了一种混合持久化(本文所 ...

  10. SpringAOP之使用切入点创建通知

    之前已经说过了SpringAOP中的几种通知类型以及如何创建简单的通知见地址 一.什么是切入点 通过之前的例子中,我们可以创建ProxyFactory的方式来创建通知,然后获取目标类中的方法.通过不同 ...