Linux内核oops panic简析
源码基于:Linux 5.4
0. 前言
内核异常的级别大致分为三个:BUG、oops、panic。

BUG 是指那些不符合内核的正常设计,但内核能够检测出来并且对系统运行不会产生影响的问题,比如在原子上下文中休眠,在内核中用 BUG 标识。
Oops 就意外着内核出了异常,此时会将产生异常时出错原因,CPU的状态,出错的指令地址、数据地址及其他寄存器,函数调用的顺序甚至是栈里面的内容都打印出来,然后根据异常的严重程度来决定下一步的操作:杀死导致异常的进程或者挂起系统。
panic 本意是“恐慌”的意思,这里意旨 kernel 发生了致命错误导致无法继续运行下去的情况。根据实际情况 Oops最终也可能会导致panic 的发生。
本文将简单分析下这三种异常的流程。
1. BUG()
有过驱动调试经验的人肯定都知道这个东西,这里的 BUG 跟我们一般认为的 “软件缺陷” 可不是一回事,这里说的 BUG() 其实是linux kernel中用于拦截内核程序超出预期的行为,属于软件主动汇报异常的一种机制。这里有个疑问,就是什么时候会用到呢?一般来说有两种用到的情况:
- 一是软件开发过程中,若发现代码逻辑出现致命 fault 后就可以调用BUG()让kernel死掉(类似于assert),这样方便于定位问题,从而修正代码执行逻辑;
- 另外一种情况就是,由于某种特殊原因(通常是为了debug而需抓ramdump),我们需要系统进入kernel panic的情况下使用;
对于 arm64 来说 BUG() 定义如下:
arch/arm64/include/asm/bug.h#ifndef _ARCH_ARM64_ASM_BUG_H
#define _ARCH_ARM64_ASM_BUG_H#include <linux/stringify.h>#include <asm/asm-bug.h>#define __BUG_FLAGS(flags) \asm volatile (__stringify(ASM_BUG_FLAGS(flags)));#define BUG() do { \__BUG_FLAGS(0); \unreachable(); \
} while (0)#define __WARN_FLAGS(flags) __BUG_FLAGS(BUGFLAG_WARNING|(flags))#define HAVE_ARCH_BUG#include <asm-generic/bug.h>#endif /* ! _ARCH_ARM64_ASM_BUG_H */
注意最后的 define HAVE_ARCH_BUG ,对于arm64 架构来说,会通过 include asm-generict/bug.h 对 BUG() 进行重定义。
include/asm-generic/bug.h#ifndef HAVE_ARCH_BUG
#define BUG() do { \printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \barrier_before_unreachable(); \panic("BUG!"); \
} while (0)
#endif#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)
#endif
也就是在 arm64 架构中 BUG() 和 BUG_ON() 都是执行的 panic()。
而对于 arm 32位架构来说,BUG() 会向CPU 下发一条未定义指令而触发ARM 发起未定义指令异常,随后进入 kernel 异常处理流程,通过调用die() 经历Oops 和 panic,下面会单独分析 die() 函数,详细看第 3 节。
2. oops
oops 意外着内核出了异常,此时会将产生异常时出错原因,CPU的状态,出错的指令地址、数据地址及其他寄存器,函数调用的顺序甚至是栈里面的内容都打印出来,然后根据异常的严重程度来决定下一步的操作:杀死导致异常的进程或者挂起系统。
例如,在编写驱动或内核模块时,常常会显示或隐式地对指针进行非法取值或使用不正确的指针,导致内核发生一个 oops 错误。当处理器在内核空间中访问一个分发的指针时,因为虚拟地址到物理地址的映射关系还没有建立,会触发一个缺页中断,在缺页中断中该地址是非法的,内核无法正确地为该地址建立映射关系,所以内核触发一个oops 错误。代码如下:
arch/arm64/mm/fault.cstatic void die_kernel_fault(const char *msg, unsigned long addr,unsigned int esr, struct pt_regs *regs)
{bust_spinlocks(1);pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,addr);mem_abort_decode(esr);show_pte(addr);die("Oops", regs, esr);bust_spinlocks(0);do_exit(SIGKILL);
}
通过 die() 会进行oops 异常处理,详细的 die() 函数流程看第 3 节。
当出现 oops,并且如果有源码,可以通过 arm 的 arch64-linux-gnu-objdump 工具看到出错的函数的汇编情况,也可以通过 GDB 工具分析。如果出错的地方为内核函数,可以使用 vmlinux 文件。
如果没有源码,对于没有编译符号表的二进制文件,可以使用:
arch64-linux-gnu-objdump -d oops.ko
命令来转储 oops.ko 文件
内核也提供了一个非常好用的脚本,可以快速定位问题,该脚本位于 Linux 源码目录下的 scripts/decodecode 中,会把出错的 oops 日志信息转换成直观有用的汇编代码,并且告知具体出错的汇编语句,这对于分析没有源码的 oops 错误非常有用。
3. die()
arch/arm64/kernel/traps.cstatic DEFINE_RAW_SPINLOCK(die_lock);/** This function is protected against re-entrancy.*/
void die(const char *str, struct pt_regs *regs, int err)
{int ret;unsigned long flags;raw_spin_lock_irqsave(&die_lock, flags);oops_enter();console_verbose();bust_spinlocks(1);ret = __die(str, err, regs);if (regs && kexec_should_crash(current))crash_kexec(regs);bust_spinlocks(0);add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE);oops_exit();if (in_interrupt())panic("Fatal exception in interrupt");if (panic_on_oops)panic("Fatal exception");raw_spin_unlock_irqrestore(&die_lock, flags);if (ret != NOTIFY_STOP)do_exit(SIGSEGV);
}
oops_enter() ---> oops_exit() 为Oops 的处理流程,获取console 的log 级别,并通过 __die() 通过对Oops 感兴趣的模块进行callback,打印模块状态不为 MODULE_STATE_UNFORMED 的模块信息,打印PC、LR、SP、x0 等寄存器信息,打印调用栈信息,等等。
3.1 __die()
arch/arm64/kernel/traps.cstatic int __die(const char *str, int err, struct pt_regs *regs)
{static int die_counter;int ret;pr_emerg("Internal error: %s: %x [#%d]" S_PREEMPT S_SMP "\n",str, err, ++die_counter);/* trap and error numbers are mostly meaningless on ARM */ret = notify_die(DIE_OOPS, str, regs, err, 0, SIGSEGV);if (ret == NOTIFY_STOP)return ret;print_modules();show_regs(regs);dump_kernel_instr(KERN_EMERG, regs);return ret;
}
- 打印 EMERG 的log,Internal error: oops.....;
- notify_die() 会通知所有对 Oops 感兴趣的模块并进行callback;
- print_modules() 打印模块状态不为 MODULE_STATE_UNFORMED 的模块信息;
- show_regs() 打印PC、LR、SP 等寄存器的信息,同时打印调用堆栈信息;
- dump_kernel_instr() 打印 pc指针和前4条指令;
这里不过多的剖析,感兴趣的可以查看下源码。
这里需要注意的是 notify_die() 会通知所有的Oops 感兴趣的模块,模块会通过函数 register_die_notifier() 将callback 注册到全局结构体变量 die_chain 中(多个模块注册进来形成一个链表),然后在通过 notify_die() 函数去解析这个 die_chain,并分别调用callback:
kernel/notifier.cstatic ATOMIC_NOTIFIER_HEAD(die_chain);int notrace notify_die(enum die_val val, const char *str,struct pt_regs *regs, long err, int trap, int sig)
{struct die_args args = {.regs = regs,.str = str,.err = err,.trapnr = trap,.signr = sig,};RCU_LOCKDEP_WARN(!rcu_is_watching(),"notify_die called but RCU thinks we're quiescent");return atomic_notifier_call_chain(&die_chain, val, &args);
}
NOKPROBE_SYMBOL(notify_die);int register_die_notifier(struct notifier_block *nb)
{vmalloc_sync_mappings();return atomic_notifier_chain_register(&die_chain, nb);
}
3.2 oops同时有可能panic
从上面 die() 函数最后看到,oops_exit() 之后也有可能进入panic():
arch/arm64/kernel/traps.cvoid die(const char *str, struct pt_regs *regs, int err)
{...if (in_interrupt())panic("Fatal exception in interrupt");if (panic_on_oops)panic("Fatal exception");...
}
处于中断 或 panic_on_oops 打开时进入 panic。
中断的可能性:
- 硬件 IRQ;
- 软件 IRQ;
- NMI;
panic_on_oops 的值受 CONFIG_PANIC_ON_OOPS_VALUE 影响。当然该值也可以通过节点
/proc/sys/kernel/panic_on_oops 进行动态修改。
4. panic()
panic 本意是“恐慌”的意思,这里意旨kernel发生了致命错误导致无法继续运行下去的情况。
kernel/panic.c/*** panic - halt the system* @fmt: The text string to print** Display a message, then perform cleanups.** This function never returns.*/
void panic(const char *fmt, ...)
{static char buf[1024];va_list args;long i, i_next = 0, len;int state = 0;int old_cpu, this_cpu;bool _crash_kexec_post_notifiers = crash_kexec_post_notifiers;//禁止本地中断,避免出现死锁,因为无法防止中断处理程序(在获得panic锁后运行)再次被调用paniclocal_irq_disable();//禁止任务抢占preempt_disable_notrace();//通过this_cpu确认是否调用panic() 的cpu是否为panic_cpu;//即,只允许一个CPU执行该代码,通过 panic_smp_self_stop() 保证当一个CPU执行panic时,//其他CPU处于停止或等待状态;this_cpu = raw_smp_processor_id();old_cpu = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu)panic_smp_self_stop();//把console的打印级别放开console_verbose();bust_spinlocks(1);va_start(args, fmt);len = vscnprintf(buf, sizeof(buf), fmt, args);va_end(args);if (len && buf[len - 1] == '\n')buf[len - 1] = '\0';//解析panic所携带的message,前缀为Kernel panic - not syncingpr_emerg("Kernel panic - not syncing: %s\n", buf);
#ifdef CONFIG_DEBUG_BUGVERBOSE/** Avoid nested stack-dumping if a panic occurs during oops processing*/if (!test_taint(TAINT_DIE) && oops_in_progress <= 1)dump_stack();
#endif//如果kgdb使能,即CONFIG_KGDB为y,在停掉所有其他CPU之前,跳转kgdb断点运行kgdb_panic(buf);if (!_crash_kexec_post_notifiers) {printk_safe_flush_on_panic();//会根据当前是否设置了转储内核(使能CONFIG_KEXEC_CORE)确定是否实际执行转储操作;//如果执行转储则会通过 kexec 将系统切换到新的kdump 内核,并且不会再返回;//如果不执行转储,则继续后面流程;__crash_kexec(NULL);//停掉其他CPU,只留下当前CPU干活smp_send_stop();} else {/** If we want to do crash dump after notifier calls and* kmsg_dump, we will need architecture dependent extra* works in addition to stopping other CPUs.*/crash_smp_send_stop();}//通知所有对panic感兴趣的模块进行回调,添加一些kmsg信息到输出atomic_notifier_call_chain(&panic_notifier_list, 0, buf);/* Call flush even twice. It tries harder with a single online CPU */printk_safe_flush_on_panic();//dump 内核log buffer中的log信息kmsg_dump(KMSG_DUMP_PANIC);/** If you doubt kdump always works fine in any situation,* "crash_kexec_post_notifiers" offers you a chance to run* panic_notifiers and dumping kmsg before kdump.* Note: since some panic_notifiers can make crashed kernel* more unstable, it can increase risks of the kdump failure too.** Bypass the panic_cpu check and call __crash_kexec directly.*/if (_crash_kexec_post_notifiers)__crash_kexec(NULL);#ifdef CONFIG_VTunblank_screen();
#endifconsole_unblank();/** We may have ended up stopping the CPU holding the lock (in* smp_send_stop()) while still having some valuable data in the console* buffer. Try to acquire the lock then release it regardless of the* result. The release will also print the buffers out. Locks debug* should be disabled to avoid reporting bad unlock balance when* panic() is not being callled from OOPS.*/debug_locks_off();console_flush_on_panic(CONSOLE_FLUSH_PENDING);panic_print_sys_info();if (!panic_blink)panic_blink = no_blink;//如果sysctl配置了panic_timeout > 0则在panic_timeout后重启系统if (panic_timeout > 0) {/** Delay timeout seconds before rebooting the machine.* We can't use the "normal" timers since we just panicked.*/pr_emerg("Rebooting in %d seconds..\n", panic_timeout);for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {touch_nmi_watchdog();if (i >= i_next) {i += panic_blink(state ^= 1);i_next = i + 3600 / PANIC_BLINK_SPD;}mdelay(PANIC_TIMER_STEP);}}if (panic_timeout != 0) {/** This will not be a clean reboot, with everything* shutting down. But if there is a chance of* rebooting the system it will be rebooted.*/if (panic_reboot_mode != REBOOT_UNDEFINED)reboot_mode = panic_reboot_mode;emergency_restart();}
#ifdef __sparc__{extern int stop_a_enabled;/* Make sure the user can actually press Stop-A (L1-A) */stop_a_enabled = 1;pr_emerg("Press Stop-A (L1-A) from sun keyboard or send break\n""twice on console to return to the boot prom\n");}
#endif
#if defined(CONFIG_S390)disabled_wait();
#endifpr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);/* Do not scroll important messages printed above */suppress_printk = 1;local_irq_enable();for (i = 0; ; i += PANIC_TIMER_STEP) {touch_softlockup_watchdog();if (i >= i_next) {i += panic_blink(state ^= 1);i_next = i + 3600 / PANIC_BLINK_SPD;}mdelay(PANIC_TIMER_STEP);}
}EXPORT_SYMBOL(panic);
详细信息见代码注释。
相关文章:
Linux内核oops panic简析
源码基于:Linux 5.4 0. 前言 内核异常的级别大致分为三个:BUG、oops、panic。 BUG 是指那些不符合内核的正常设计,但内核能够检测出来并且对系统运行不会产生影响的问题,比如在原子上下文中休眠,在内核中用 BUG 标识。…...
Spark大数据处理讲课笔记4.8 Spark SQL典型案例
文章目录 零、本讲学习目标一、使用Spark SQL实现词频统计(一)数据源 - words.txt(二)创建Maven项目(三)添加依赖和构建插件(四)修改源目录名称(五)创建日志属…...
WhatsApp Business 多人使用终极指南
今时今日,几乎每个人的手机上都安装了 WhatsApp,不少电商更会依赖 WhatsApp作为和客户沟通的主要渠道。但对有一定规模的店铺来说,WhatsApp绑定一个号码和设备的设定实在很不方便。如何才能用WhatsApp Business批量处理客户查询呢?…...
布局和视图的常用属性
0、用户界面由布局和GUI组件组成 之前的项目中都是使用线性布局LinearLayout,LinearLayout将视图显示在一行或一列中。其使用方法如下所示: <LinearLayoutxmlns:android"http://schemas.android.com/apk/res/android"android:layout_width…...
解说天下之操作系统
解说天下之操作系统 本文由桌案drawon (https://www.drawon.cn),云晶(https://www.yunjingxz.com)创始人根据多年从业经验, 从操作系统的起源,应用分类, 设计分类,以及资源使用角度对操作系统进…...
Pruning 系列 (八)layer常用简枝(torch)方法
环境 python 3.9numpy 1.24.1pytorch 2.0.0+cu117一、prune.random_unstructured pytorch:文档地址 用法: torch.nn.utils.prune.random_unstructured(module, name, amount) 参数: module(torch.nn.Module) -包含要修剪的张量的模块 name(str) -module 中的参数名称,将…...
Gigabyte Z490 Vision D i9-10900k电脑 Hackintosh 黑苹果efi引导文件
原文来源于黑果魏叔官网,转载需注明出处。(下载请直接百度黑果魏叔) 硬件型号驱动情况 主板Gigabyte Z490 Vision D 处理器Intel i9-10900k已驱动 内存64GB G.Skill Trident Z 3600Mhz CL18已驱动 硬盘西数 WDS250G3X0C-00SJG0 ( SN750) …...
UWB智慧工厂人员定位系统源码,人员在岗监控、车辆实时轨迹监控源码
近年来人员定位系统在工业领域的发展势头迅猛,工业识别与定位成为促进制造业数字化的关键技术。通过实时定位可以判断所有的人、物、车的位置。实时定位系统要适用于复杂工业环境,单一技术是很难实现的,需要融合多种不同的定位技术࿰…...
从认识元注解到使用元注解
前言:注解是Java语言中的一种特殊语法,它可以为代码提供更加灵活的元数据信息,方便代码的处理和使用。而元注解则是用于定义注解的注解,它可以为注解提供更多的元数据信息和特性。本文将介绍如何自定义元注解,包括认识…...
【C++从0到王者】第六站:类和对象(下)
文章目录 一、再谈构造函数1.构造函数体赋值2.初始化列表1>初始化列表的使用2>初始化列表的注意事项 3.explicit关键词 二、static成员1.如何统计当前程序中变量的个数2.static的特性3.从1加到n4.设计一个类,只能在栈或者堆上开辟空间 三、友元1.友元函数2.友…...
AJax和Axios的讲解
目录 Ajax Ajax基本介绍 同步异步 原生Ajax 原生的Ajax使用方式 Axios 基本介绍 Axios的基本使用 发送 get 请求 发送 post 请求 Axios快速入门 请求方法的别名 练习 Ajax Ajax基本介绍 Ajax: 全称Asynchronous JavaScript And XML,异步的JavaScript和XML…...
企业落地数字化转型,如何部署战略规划
当前环境下,各领域企业通过数字化相关的一切技术,以数据为基础、以用户为核心,创建一种新的,或对现有商业模式进行重塑就是数字化转型。这种数字化转型给企业带来的效果就像是一次重构,会对企业的业务流程、思维文化、…...
新的网络钓鱼即服务平台让网络犯罪分子生成令人信服的网络钓鱼页面
至少从2022年中期开始,网络犯罪分子就利用一个名为“伟大”的新型网络钓鱼即服务(PhaaS或PaaS)平台来攻击微软365云服务的企业用户,有效地降低了网络钓鱼攻击的门槛。 思科Talos研究员蒂亚戈佩雷拉表示:“目前,Greatness只专注于微软365钓鱼…...
MySQL的隐式转换
隐式转换 若字符串是以数字开头,并且全部都是数字,则转换的数字结果是整个字符串;部分是数字,则转换的数字结果是截止到第一个不是数字的字符为止 若字符串不是以数字开头,则转换的数字结果是 0 varchar str "…...
LeetCode:23. 合并 K 个升序链表
23. 合并 K 个升序链表 1)题目2)过程3)代码1. 最开始2.初步优化 4)结果1. 最开始2. 初步优化 1)题目 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合…...
js:正则表达式常用方法总结test、exec、match、matchAll、replace、replaceAll、search
文章目录 正则使用testmatch/matchAll不加g加ggroup 的使用 matchAll不加g加g exec不加g加g searchreplace 正则使用 常用的几种方法有:test、exec、match、matchAll、replace、replaceAll、search test // 匹配返回true,不匹配false /e/.test("…...
分析车载蓝牙通话只有前喇叭声音,后面喇叭无声背后原因
车载蓝牙通话只有前喇叭声音,后面喇叭无声背后原因 大家有没有注意到车载蓝牙连接后通话的时候只有前喇叭的有声音,后面喇叭没声音呢?特别是后装的车载多媒体上基本都是这样,细思下为什么这样的原因, 采访后装车载技术…...
高性能ADC/DAC FMC子卡推出-FMC164
FMC164 子卡集成 4 通道 1Gsps 采样率,16 位 高性能ADC采样,板载4 通道1.25Gsps 16 位DA。板载时钟芯片 HMC7044,可以提供 JESD204B所需要的各种时钟。具有同步/触发功能,模拟信号采用 SSMC 射频连接器输入和输出。板载时钟芯片为…...
Agisoft Metashape 红外影像处理
系列文章目录 文章目录 系列文章目录前言一、加载红外影像二、对齐照片三、构建 DEM四、生成 DOM五、温度值可视化前言 Agisoft Metashape 专业版支持处理来自 AscTec(ARA 格式)、WIRIS(TIFF 格式)热成像仪和以 R-JPEG(FLIR 数据)格式保存数据的热成像数据。 在本文中,…...
Mybatis从入门到入土
一、什么是Mybatis 1)MyBatis 是一款优秀的持久层(DAO层)框架 2)MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程 3)MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将…...
【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
