Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 系统与高性能 / Systems & Performance

xv6 源码学习 / xv6 Source Learning

1. xv6 源码学习专题 / xv6 Source Learning

2. xv6 学习路线:从 C 指针到操作系统源码 / xv6 Learning Roadmap

3. 读 xv6 必备的 C 语言和指针基础 / C And Pointers For xv6

4. 读 xv6 必备的 RISC-V 基础 / RISC-V Basics For xv6

5. 如何阅读 xv6 源码 / How To Read xv6 Source

6. xv6 启动与内核入口 / xv6 Boot And Kernel Entry

7. xv6 内存布局与页表 / xv6 Memory Layout And Page Tables

8. xv6 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls

9. xv6 进程与 struct proc / xv6 Process And struct proc

10. xv6 上下文切换与调度器 / xv6 Context Switch And Scheduler

11. xv6 sleep/wakeup、wait 与 exit / xv6 Sleep Wakeup Wait And Exit

12. xv6 锁与并发 / xv6 Locks And Concurrency

13. xv6 文件描述符与 inode / xv6 File Descriptor And Inode

14. xv6 文件系统与日志 / xv6 File System And Log

15. xv6 设备驱动、console、UART 与磁盘 / xv6 Device Driver Console UART And Disk

16. xv6 用户程序与 shell / xv6 User Programs And Shell

17. 用 QEMU/GDB 调试 xv6 / Debugging xv6 With QEMU And GDB

18. 从 xv6 过渡到 Linux 0.11 / Bridge From xv6 To Linux 0.11

19. xv6 专题总结与实践项目 / xv6 Summary And Practice Projects

本页目录

用 QEMU/GDB 调试 xv6 / Debugging xv6 With QEMU And GDB ​

1. 这篇文章解决什么问题 ​

读 xv6 源码会产生很多心智模型。

例如系统调用先到 usertrap。

例如 fork 会复制 trapframe。

例如 exec 会替换用户页表。

例如调度器只选择 RUNNABLE 进程。

这些模型如果只停留在脑中,很容易错。

GDB 的价值不是炫技。

GDB 的价值是验证。

验证表示用可观察事实检查自己的理解。

本篇讲如何用 QEMU 和 GDB 调试 xv6。

我们会看断点。

会看调用栈。

会看 struct proc。

会看 trapframe。

会看页表。

会跟踪系统调用路径。

本篇基于本地可读的 E:\Github\xv6-riscv。

如果你的源码是标准 MIT xv6-riscv,路径和函数名应基本一致。

1.1 本文基础词小注释 ​

调试 debugging:调试不是只在程序崩了以后才做。对学习 xv6 来说,调试是“让源码里的路径真的跑给你看”,用事实校准理解。

断点 breakpoint:让程序运行到某一行或某个函数时暂停。比如在 usertrap 下断点,就可以观察一次系统调用或中断进入内核时的状态。

单步 step:让程序一次执行一小步。它适合观察控制流如何从一个函数走到另一个函数,但在汇编或中断路径里要小心,步子太细会迷路。

text
run
 |
 v
hit breakpoint
 |
 v
inspect registers / memory / structs
 |
 v
continue or step
1
2
3
4
5
6
7
8
9
10

寄存器 register:CPU 内部很小但很快的存储位置。函数参数、返回地址、栈指针、临时值都会放在寄存器里。看 trap 和 context switch 时,寄存器尤其重要。

调用栈 backtrace:当前函数是被谁调用来的,再往上是谁调用的,这条链就是调用栈。GDB 的 bt 可以帮助你从“当前卡在哪里”回溯到“为什么会走到这里”。

GDB stub:QEMU 提供的调试接口。它让外部 GDB 能连接进来控制虚拟机里的 xv6:暂停、继续、读寄存器、读内存。

客体机 guest:QEMU 里运行的 xv6。宿主机 host 是你的 Windows/Linux/macOS 环境。调试时要分清命令在哪个世界执行。

2. 最简单的心智模型 ​

QEMU 负责运行虚拟机器。

GDB 负责连接 QEMU 的调试端口。

QEMU 可以停在第一条指令。

GDB 可以读取寄存器和内存。

GDB 可以设置断点。

GDB 可以单步执行。

GDB 可以查看 C 结构体。

图:

text
+-------------------+        TCP gdb stub        +-------------------+
| QEMU xv6 machine  | <------------------------> | riscv64 gdb       |
| CPU/memory/devices|                             | break/step/print  |
+-------------------+                             +-------------------+
1
2
3
4

调试时要分清两个世界。

一个是宿主机世界。

一个是 xv6 客体机世界。

GDB 命令在宿主机执行。

被调试的代码在 QEMU 中运行。

3. 本篇涉及的 xv6 源码文件 ​

Makefile 提供 qemu-gdb 目标。

.gdbinit.tmpl-riscv 生成 GDB 初始化文件。

kernel/kernel 是带符号的内核 ELF。

kernel/trap.c 包含 usertrap、kerneltrap、prepare_return。

kernel/trampoline.S 包含 uservec、userret。

kernel/syscall.c 包含系统调用分派。

kernel/sysproc.c 包含进程类系统调用入口。

kernel/sysfile.c 包含文件类系统调用入口。

kernel/proc.h 定义 struct proc 和 struct trapframe。

kernel/proc.c 定义 scheduler、sched、kfork、kwait、kexit。

kernel/vm.c 定义页表创建、映射、复制、walk、copyin/copyout。

kernel/memlayout.h 定义 TRAMPOLINE、TRAPFRAME 等地址。

kernel/riscv.h 定义 CSR 读写函数和页表位。

user/init.c、user/sh.c、user/echo.c 可作为用户态触发点。

4. 准备调试环境 ​

xv6 Makefile 通常提供:

text
make qemu
make qemu-gdb
1
2

make qemu 直接启动系统。

make qemu-gdb 启动系统并等待 GDB 连接。

Makefile 中的 QEMU 选项包含 -S。

-S 表示 CPU 启动后先暂停。

Makefile 中的 QEMU 选项还包含 gdb stub。

stub 是调试代理接口。

它让外部 GDB 控制虚拟 CPU。

典型流程:

text
terminal 1:
  make qemu-gdb

terminal 2:
  riscv64-unknown-elf-gdb kernel/kernel
1
2
3
4
5

不同环境的 GDB 名称可能不同。

有的叫 riscv64-linux-gnu-gdb。

有的叫 gdb-multiarch。

关键是它要能理解 RISC-V ELF。

5. 为什么需要带符号的内核 ​

符号是地址到名字的映射。

例如地址 0x80001abc 对应 usertrap。

没有符号,GDB 只能显示地址。

有符号,GDB 可以显示函数名和源码行。

xv6 Makefile 的 CFLAGS 通常包含:

text
-ggdb
-gdwarf-2
-fno-omit-frame-pointer
1
2
3

-ggdb 生成 GDB 调试信息。

-gdwarf-2 指定调试信息格式。

-fno-omit-frame-pointer 保留帧指针。

帧指针有助于查看调用栈。

如果 GDB 看不到源码,先确认加载的是 kernel/kernel。

不要加载 fs.img。

不要加载宿主机上的用户程序 ELF 当内核符号。

6. 第一次连接后的基本命令 ​

连接后可以先用这些命令:

text
(gdb) target remote localhost:26000
(gdb) b main
(gdb) c
(gdb) bt
(gdb) info registers
1
2
3
4
5

端口号以 Makefile 输出为准。

b 是 break。

breakpoint 是断点。

断点表示执行到某个位置时暂停。

c 是 continue。

continue 表示继续运行。

bt 是 backtrace。

backtrace 表示调用栈。

info registers 查看寄存器。

RISC-V 常见寄存器包括 ra、sp、a0、a7、sepc。

ra 是返回地址。

sp 是栈指针。

a0 常放第一个参数和返回值。

a7 常放系统调用号。

sepc 是 trap 返回时的程序计数器。

7. 调试时先建立观察问题 ​

不要一上来乱单步。

先问一个可验证的问题。

例如:write 系统调用号是不是放在 a7?

例如:usertrap 是否会把 epc 加 4?

例如:exec 是否会改变 p->pagetable?

例如:fork 后父子 trapframe 是否相似?

例如:wait 回收时子进程状态是否为 ZOMBIE?

好的调试是实验。

实验需要假设。

实验需要观察点。

实验需要预期结果。

实验需要对反例保持敏感。

8. 断在系统调用路径 ​

要观察系统调用,可以设置这些断点:

text
(gdb) b usertrap
(gdb) b syscall
(gdb) b sys_write
(gdb) b sys_fork
(gdb) b sys_exec
(gdb) b sys_wait
1
2
3
4
5
6

然后在 xv6 shell 中运行:

text
$ echo hello
1

你可能会先停在许多系统调用上。

因为 shell 自己也会调用 write 打印提示符。

它会调用 read 读取输入。

它会调用 fork 创建子进程。

它会调用 wait 等待子进程。

调试时噪声是正常的。

可以使用条件断点降低噪声。

例如只在某个系统调用号时停下。

9. 查看当前进程 ​

在内核中,可以调用 myproc()。

GDB 里可以打印:

text
(gdb) p myproc()
(gdb) p *myproc()
1
2

struct proc 是进程控制块。

进程控制块是内核保存进程状态的数据结构。

它包含 pid。

它包含 state。

它包含 pagetable。

它包含 trapframe。

它包含 kernel stack。

它包含 open files。

它包含 cwd。

它包含 name。

更聚焦的命令:

text
(gdb) p myproc()->pid
(gdb) p myproc()->name
(gdb) p myproc()->state
(gdb) p myproc()->sz
(gdb) p/x myproc()->pagetable
1
2
3
4
5

p/x 用十六进制显示。

地址、PTE、CSR 通常适合十六进制。

10. 查看 trapframe ​

trapframe 保存用户态寄存器。

系统调用参数也在其中。

可用命令:

text
(gdb) p myproc()->trapframe
(gdb) p *myproc()->trapframe
(gdb) p/x myproc()->trapframe->epc
(gdb) p/x myproc()->trapframe->a0
(gdb) p/x myproc()->trapframe->a1
(gdb) p/x myproc()->trapframe->a2
(gdb) p/x myproc()->trapframe->a7
1
2
3
4
5
6
7

对 write(1, buf, n) 来说:

a0 应该接近 1。

a1 应该是用户虚拟地址。

a2 应该是长度。

a7 应该是 SYS_write。

进入 syscall() 后:

text
p->trapframe->a0 = syscalls[num]()
1

所以系统调用返回值会覆盖 a0。

这解释了为什么 a0 既是参数又是返回值。

它们发生在不同阶段。

11. 验证 epc += 4 ​

RISC-V 的 ecall 指令长度是 4 字节。

当用户程序执行 ecall 进入内核时,sepc 指向 ecall 本身。

如果返回时还从 ecall 执行,就会重复陷入内核。

所以 usertrap 中有:

text
p->trapframe->epc += 4
1

调试方法:

text
(gdb) b usertrap
(gdb) c
(gdb) p/x r_sepc()
(gdb) n
(gdb) p/x myproc()->trapframe->epc
1
2
3
4
5

预期:保存的 epc 比原始 ecall 地址多 4。

如果你忘了这点,系统调用会像卡住一样不断重复。

这是一个非常好的心智模型验证点。

12. 查看调用栈 ​

在 sys_write 断下时,执行:

text
(gdb) bt
1

你可能看到类似路径:

text
sys_write
syscall
usertrap
1
2
3

调用栈不会直接显示用户态 write stub。

因为已经跨过 trap 边界。

用户态寄存器保存在 trapframe。

内核态调用栈在内核栈上。

这说明调试跨特权级路径时,要同时看两种状态。

一种是当前内核调用栈。

一种是 trapframe 中保存的用户上下文。

13. 查看页表的基本思路 ​

页表把虚拟地址映射到物理地址。

PTE 是页表项。

PTE 包含物理页号和权限位。

xv6 RISC-V 使用 Sv39 页表。

Sv39 是 RISC-V 64 位的一种分页模式。

它使用三级页表。

每级索引 9 位。

页内偏移 12 位。

结构:

text
virtual address
  |
  +-- VPN[2] -> level 2 page table
  +-- VPN[1] -> level 1 page table
  +-- VPN[0] -> level 0 page table
  +-- offset -> byte inside page
1
2
3
4
5
6

GDB 可以打印 myproc()->pagetable。

但光看地址还不够。

要理解映射,最好调用或临时使用页表打印函数。

MIT xv6 实验中常让你实现 vmprint。

如果源码中没有现成 vmprint,可以自己加在学习分支中。

14. 查看 satp ​

satp 是 RISC-V 保存当前页表根的 CSR。

CSR 是控制和状态寄存器。

内核运行时通常使用内核页表。

用户态运行时使用用户页表。

trap 入口会切换页表。

返回用户态前也会切换页表。

在 GDB 中查看寄存器:

text
(gdb) info registers satp
1

也可以在源码中关注:

text
MAKE_SATP(p->pagetable)
w_satp(...)
1
2

trampoline.S 负责关键切换。

为什么 trampoline 要特殊映射?

因为切换页表的那段代码必须在切换前后都可访问。

所以 xv6 把 trampoline 映射到用户页表和内核页表的固定高地址。

15. 跟踪 fork ​

断点:

text
(gdb) b sys_fork
(gdb) b kfork
(gdb) b allocproc
1
2
3

运行:

text
$ echo hello
1

观察点:

父进程 pid 是多少。

新进程 pid 是多少。

np->trapframe 是否从父进程复制。

子进程的 trapframe->a0 是否被改成 0。

父进程的 fork 返回值是否是子 pid。

这是 fork 的经典语义:

text
parent: fork returns child pid
child:  fork returns 0
1
2

GDB 验证命令:

text
(gdb) p p->pid
(gdb) p np->pid
(gdb) p np->trapframe->a0
1
2
3

如果在局部变量作用域外,变量名可能不可见。

这时在源码行附近断点更方便。

16. 跟踪 exec ​

断点:

text
(gdb) b sys_exec
(gdb) b kexec
1
2

运行:

text
$ echo hello
1

观察点:

sys_exec 从用户地址取 path。

kexec 打开 ELF 文件。

kexec 创建新页表。

kexec 加载程序段。

kexec 准备用户栈。

kexec 设置 trapframe->epc。

kexec 设置 trapframe->sp。

kexec 释放旧页表。

重点不是背每一行。

重点是确认 exec 替换了用户空间。

进程 pid 通常不变。

进程的程序内容变了。

这就是“同一个进程,新的程序”。

17. 跟踪 wait 和 exit ​

断点:

text
(gdb) b sys_wait
(gdb) b kwait
(gdb) b sys_exit
(gdb) b kexit
1
2
3
4

观察点:

子进程退出时状态变成 ZOMBIE。

父进程 wait 扫描子进程。

父进程发现 ZOMBIE。

父进程读取退出状态。

父进程调用 freeproc。

可打印:

text
(gdb) p myproc()->pid
(gdb) p myproc()->state
(gdb) p myproc()->xstate
1
2
3

ZOMBIE 不表示进程还在运行。

它表示进程已经退出,但记录尚未被父进程回收。

18. 跟踪调度器 ​

调度器断点容易很吵。

可以从单 CPU 开始。

启动时设置:

text
make CPUS=1 qemu-gdb
1

单 CPU 能减少并发交错。

断点:

text
(gdb) b scheduler
(gdb) b sched
(gdb) b swtch
1
2
3

观察点:

scheduler 找到 RUNNABLE 进程。

它把进程改成 RUNNING。

它设置 c->proc = p。

它调用 swtch。

进程让出 CPU 时又调用 sched。

流程:

text
scheduler context
  |
  v
swtch to process context
  |
  v
process runs
  |
  v
sched
  |
  v
swtch back to scheduler
1
2
3
4
5
6
7
8
9
10
11
12
13

19. 条件断点 ​

条件断点在 xv6 调试中很有用。

普通断点会停太多次。

例如只想看某个 pid:

text
(gdb) b syscall if myproc()->pid == 3
1

例如只想看某个系统调用号:

text
(gdb) b syscall if myproc()->trapframe->a7 == SYS_exec
1

有时 GDB 不认识宏。

可以直接使用数字。

系统调用号在 kernel/syscall.h。

条件断点也可能让执行变慢。

调试内核时慢是正常的。

先求观察清楚。

再求效率。

20. watchpoint ​

watchpoint 是观察点。

它在某个内存位置被读写时暂停。

例如观察当前进程状态:

text
(gdb) watch myproc()->state
1

但这个表达式可能随当前 CPU 和进程变化。

更稳定的做法是先保存地址。

例如:

text
(gdb) set $p = myproc()
(gdb) watch $p->state
1
2

watchpoint 对页表、状态机调试很有帮助。

但硬件 watchpoint 数量有限。

在 QEMU 中也可能较慢。

21. 查看内存 ​

GDB 的 x 命令查看内存。

常见形式:

text
(gdb) x/10gx address
(gdb) x/20i address
(gdb) x/s address
1
2
3

g 表示 giant word,通常 8 字节。

i 表示反汇编指令。

s 表示字符串。

如果地址是用户虚拟地址,要注意当前页表。

GDB 看到的是 QEMU 当前 CPU 的地址空间。

在内核态直接 x/s user_pointer 不一定可靠。

这也是为什么内核用 copyinstr。

内核不能把用户地址当普通内核地址。

22. 调试用户程序的限制 ​

xv6 用户程序也可以带符号。

但调试用户程序比调试内核麻烦。

因为 exec 会切换地址空间。

用户程序常被链接到低地址。

不同用户程序可能复用相同虚拟地址。

如果要调用户程序,可以加载对应用户 ELF 的符号。

但入门阶段建议先调内核路径。

例如通过 sys_exec 打印 path。

通过 trapframe 看用户 PC。

通过内核函数观察用户程序行为。

这样更稳定。

23. 用 GDB 验证 shell 模型 ​

假设模型:

text
sh reads command
  |
  v
fork child
  |
  +-- child exec target
  |
  +-- parent wait
1
2
3
4
5
6
7
8

验证方法:

断在 sys_fork。

断在 sys_exec。

断在 sys_wait。

运行 echo hello。

观察 pid 和 name。

预期:

shell pid 调用 fork。

子进程调用 exec。

父 shell 调用 wait。

如果结果不同,不要急着改模型。

先确认是否被 shell 提示符的 write 或输入的 read 干扰。

调试第一原则:先区分目标事件和背景噪声。

24. 用 GDB 验证 trapframe 模型 ​

假设模型:

系统调用参数来自用户寄存器。

trap 入口把寄存器保存到 trapframe。

内核从 trapframe 取参数。

返回值写回 trapframe 的 a0。

验证方法:

断在 syscall。

打印:

text
(gdb) p myproc()->trapframe->a7
(gdb) p myproc()->trapframe->a0
(gdb) p myproc()->trapframe->a1
1
2
3

单步越过分派。

再打印:

text
(gdb) p myproc()->trapframe->a0
1

预期:a0 从第一个参数变成返回值。

这能纠正一个常见误解。

寄存器含义不是永久固定。

含义由调用约定和执行阶段决定。

25. 用 GDB 验证页表模型 ​

假设模型:

用户页表包含用户代码、数据、栈、trapframe、trampoline。

内核页表包含内核代码、数据、设备映射、内核栈。

trap 过程中需要在两张页表间切换。

验证方法:

断在 prepare_return。

打印:

text
(gdb) p/x myproc()->pagetable
(gdb) p/x r_satp()
1
2

观察 MAKE_SATP(p->pagetable)。

断在 usertrap。

观察进入内核后的 satp。

如果实现了 vmprint,打印用户页表。

关注高地址的 TRAMPOLINE 和 TRAPFRAME。

这能把抽象地址空间落到具体映射上。

26. 常见问题:GDB 连不上 ​

先看 QEMU 是否已经启动 qemu-gdb。

再看 Makefile 打印的端口。

不要假设永远是 26000。

检查是否已有旧 QEMU 占用端口。

检查是否使用了正确 GDB。

检查 .gdbinit 是否生成。

如果 GDB 拒绝自动加载 .gdbinit,可能需要配置安全路径。

可以手动执行 target remote。

先让连接成功。

再追求自动化。

27. 常见问题:断点不命中 ​

可能断错函数名。

可能代码被内联。

可能还没运行到该路径。

可能符号文件不是当前构建产物。

可能你在多 CPU 下被其他 CPU 先打断。

可能断点设置在用户程序但只加载了内核符号。

解决步骤:

确认 kernel/kernel 是最新的。

确认 make clean 后重建是否改变。

先断 main 或 usertrap 这种必经点。

再逐步缩小到目标函数。

28. 常见误解 ​

误解一:GDB 调试就是单步每一行。

不是。

高质量调试是带着问题找证据。

误解二:调用栈能显示全部历史。

不能。

调用栈只显示当前栈上的调用链。

trap 边界和上下文切换会改变栈。

误解三:myproc() 总是有值。

不是。

调度器或早期启动阶段可能没有当前进程。

误解四:多 CPU 下停住的就是自己关心的进程。

不一定。

要看 pid、name、hartid。

误解五:用户指针能直接 x/s。

不一定。

要考虑当前地址空间。

误解六:断点越多越好。

不是。

断点太多会让事件顺序变得难读。

29. 小实验:验证 write ​

目标:看 echo hello 的 write 路径。

步骤:

启动 make CPUS=1 qemu-gdb。

GDB 连接。

设置 b syscall。

运行到 shell。

输入 echo hello。

每次停下打印 a7。

当 a7 是 SYS_write 时,打印 a0、a1、a2。

观察返回后 a0 变成写入字节数。

结论:系统调用号和参数确实来自 trapframe。

30. 小实验:验证 fork ​

目标:确认父子返回值不同。

断在 kfork。

在子进程 trapframe 设置返回值的位置附近单步。

打印父进程 pid。

打印新进程 pid。

打印 np->trapframe->a0。

预期:子进程看到 fork 返回 0。

父进程看到 fork 返回子 pid。

思考:为什么返回值不一样但代码看起来相同?

答案:父子有各自的 trapframe。

31. 小实验:验证 exec ​

目标:观察 exec 改变地址空间。

断在 kexec。

输入 echo hello。

记录 myproc()->pid。

记录旧 myproc()->pagetable。

在成功路径后记录新 myproc()->pagetable。

记录 trapframe->epc。

预期:pid 不变,pagetable 和 epc 改变。

解释:exec 不是创建新进程,而是替换当前进程的程序。

32. 小实验:验证 wait/exit ​

目标:看 ZOMBIE 被回收。

断在 kexit。

断在 kwait。

运行一个短命令。

在 kexit 打印当前 pid 和 state。

在 kwait 找到 ZOMBIE 子进程时打印 child pid。

观察 freeproc 后状态变为 UNUSED。

结论:exit 先留下记录,wait 再回收记录。

这比一句“wait 等待子进程”更精确。

33. 小实验:验证页表打印 ​

目标:把用户地址空间画出来。

如果已实现 vmprint,在 exec 成功后调用它。

运行一个用户程序。

观察低地址代码段。

观察用户栈。

观察 TRAPFRAME。

观察 TRAMPOLINE。

如果没有 vmprint,先只用 GDB 打印 pagetable 和 sz。

不要急着解析所有 PTE。

先理解每块区域的作用。

34. 调试记录模板 ​

每次调试建议记录:

问题是什么。

预期路径是什么。

设置了哪些断点。

输入了什么命令。

停在哪个函数。

当前 pid 是多少。

当前进程名是什么。

关键寄存器是什么。

关键结构体字段是什么。

观察结果是否符合预期。

如果不符合,下一步要排除什么。

这样的记录能防止调试变成随机游走。

35. 本篇总结 ​

GDB 是显微镜。

但显微镜不自动给答案。

你要先提出可验证的模型。

再用断点、寄存器、结构体、页表去检查它。

本篇最重要的调试路径是:

text
user stub
  |
  v
ecall
  |
  v
uservec
  |
  v
usertrap
  |
  v
syscall
  |
  v
sys_* function
  |
  v
kernel implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

理解这条路径后,xv6 不再只是源码文本。

它会变成一台可以暂停、观察、验证的机器。

最后更新于:

Pager
上一篇16. xv6 用户程序与 shell / xv6 User Programs And Shell
下一篇18. 从 xv6 过渡到 Linux 0.11 / Bridge From xv6 To Linux 0.11

持续记录,持续成长

Copyright © Tidenflow