mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-23 21:20:42 +08:00
Translated tech/20170324 Writing a Linux Debugger Part 2 Breakpoints.md
This commit is contained in:
parent
0afcf5a1f9
commit
d9cc41e76d
@ -1,204 +0,0 @@
|
||||
icltyh Translating
|
||||
Writing a Linux Debugger Part 2: Breakpoints
|
||||
============================================================
|
||||
|
||||
In the first part of this series we wrote a small process launcher as a base for our debugger. In this post we’ll learn how breakpoints work in x86 Linux and augment our tool with the ability to set them.
|
||||
|
||||
* * *
|
||||
|
||||
### Series index
|
||||
|
||||
These links will go live as the rest of the posts are released.
|
||||
|
||||
1. [Setup][1]
|
||||
2. [Breakpoints][2]
|
||||
3. Registers and memory
|
||||
4. Elves and dwarves
|
||||
5. Stepping, source and signals
|
||||
6. Stepping on dwarves
|
||||
7. Source-level breakpoints
|
||||
8. Stack unwinding
|
||||
9. Reading variables
|
||||
10. Next steps
|
||||
|
||||
* * *
|
||||
|
||||
### How is breakpoint formed?
|
||||
|
||||
There are two main kinds of breakpoints: hardware and software. Hardware breakpoints typically involve setting architecture-specific registers to produce your breaks for you, whereas software breakpoints involve modifying the code which is being executed on the fly. We’ll be focusing solely on software breakpoints for this article, as they are simpler and you can have as many as you want. On x86 you can only have four hardware breakpoints set at a given time, but they give you the power to make them fire on just reading from or writing to a given address rather than only executing code there.
|
||||
|
||||
I said above that software breakpoints are set by modifying the executing code on the fly, so the questions are:
|
||||
|
||||
* How do we modify the code?
|
||||
* What modifications do we make to set a breakpoint?
|
||||
* How is the debugger notified?
|
||||
|
||||
The answer to the first question is, of course, `ptrace`. We’ve previously used it to set up our program for tracing and continuing its execution, but we can also use it to read and write memory.
|
||||
|
||||
The modification we make has to cause the processor to halt and signal the program when the breakpoint address is executed. On x86 this is accomplished by overwriting the instruction at that address with the `int 3` instruction. x86 has an _interrupt vector table_ which the operating system can use to register handlers for various events, such as page faults, protection faults, and invalid opcodes. It’s kind of like registering error handling callbacks, but right down at the hardware level. When the processor executes the `int 3` instruction, control is passed to the breakpoint interrupt handler, which – in the case of Linux – signals the process with a `SIGTRAP`. You can see this process in the diagram below, where we overwrite the first byte of the `mov` instruction with `0xcc`, which is the instruction encoding for `int 3`.
|
||||
|
||||
![breakpoint](http://blog.tartanllama.xyz/assets/breakpoint.png)
|
||||
|
||||
The last piece of the puzzle is how the debugger is notified of the break. If you remember back in the previous post, we can use `waitpid` to listen for signals which are sent to the debugee. We can do exactly the same thing here: set the breakpoint, continue the program, call `waitpid` and wait until the `SIGTRAP`occurs. This breakpoint can then be communicated to the user, perhaps by printing the source location which has been reached, or changing the focused line in a GUI debugger.
|
||||
|
||||
* * *
|
||||
|
||||
### Implementing software breakpoints
|
||||
|
||||
We’ll implement a `breakpoint` class to represent a breakpoint on some location which we can enable or disable as we wish.
|
||||
|
||||
```
|
||||
class breakpoint {
|
||||
public:
|
||||
breakpoint(pid_t pid, std::intptr_t addr)
|
||||
: m_pid{pid}, m_addr{addr}, m_enabled{false}, m_saved_data{}
|
||||
{}
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
|
||||
auto is_enabled() const -> bool { return m_enabled; }
|
||||
auto get_address() const -> std::intptr_t { return m_addr; }
|
||||
|
||||
private:
|
||||
pid_t m_pid;
|
||||
std::intptr_t m_addr;
|
||||
bool m_enabled;
|
||||
uint64_t m_saved_data; //data which used to be at the breakpoint address
|
||||
};
|
||||
```
|
||||
|
||||
Most of this is just tracking of state; the real magic happens in the `enable` and `disable` functions.
|
||||
|
||||
As we’ve learned above, we need to replace the instruction which is currently at the given address with an `int 3`instruction, which is encoded as `0xcc`. We’ll also want to save out what used to be at that address so that we can restore the code later; we don’t want to just forget to execute the user’s code!
|
||||
|
||||
```
|
||||
void breakpoint::enable() {
|
||||
m_saved_data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);
|
||||
uint64_t int3 = 0xcc;
|
||||
uint64_t data_with_int3 = ((m_saved_data & ~0xff) | int3); //set bottom byte to 0xcc
|
||||
ptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3);
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
```
|
||||
|
||||
The `PTRACE_PEEKDATA` request to `ptrace` is how to read the memory of the traced process. We give it a process ID and an address, and it gives us back the 64 bits which are currently at that address. `(m_saved_data & ~0xff)` zeroes out the bottom byte of this data, then we bitwise `OR` that with our `int 3` instruction to set the breakpoint. Finally, we set the breakpoint by overwriting that part of memory with our new data with `PTRACE_POKEDATA`.
|
||||
|
||||
The implementation of `disable` is easier, as we simply need to restore the original data which we overwrote with `0xcc`.
|
||||
|
||||
```
|
||||
void breakpoint::disable() {
|
||||
ptrace(PTRACE_POKEDATA, m_pid, m_addr, m_saved_data);
|
||||
m_enabled = false;
|
||||
}
|
||||
```
|
||||
|
||||
* * *
|
||||
|
||||
### Adding breakpoints to the debugger
|
||||
|
||||
We’ll make three changes to our debugger class to support setting breakpoints through the user interface:
|
||||
|
||||
1. Add a breakpoint storage data structure to `debugger`
|
||||
2. Write a `set_breakpoint_at_address` function
|
||||
3. Add a `break` command to our `handle_command` function
|
||||
|
||||
I’ll store my breakpoints in a `std::unordered_map<std::intptr_t, breakpoint>` structure so that it’s easy and fast to check if a given address has a breakpoint on it and, if so, retrieve that breakpoint object.
|
||||
|
||||
```
|
||||
class debugger {
|
||||
//...
|
||||
void set_breakpoint_at_address(std::intptr_t addr);
|
||||
//...
|
||||
private:
|
||||
//...
|
||||
std::unordered_map<std::intptr_t,breakpoint> m_breakpoints;
|
||||
}
|
||||
```
|
||||
|
||||
In `set_breakpoint_at_address` we’ll create a new breakpoint, enable it, add it to the data structure, and print out a message for the user. If you like, you could factor out all message printing so that you can use the debugger as a library as well as a command-line tool, but I’ll mash it all together for simplicity.
|
||||
|
||||
```
|
||||
void debugger::set_breakpoint_at_address(std::intptr_t addr) {
|
||||
std::cout << "Set breakpoint at address 0x" << std::hex << addr << std::endl;
|
||||
breakpoint bp {m_pid, addr};
|
||||
bp.enable();
|
||||
m_breakpoints[addr] = bp;
|
||||
}
|
||||
```
|
||||
|
||||
Now we’ll augment our command handler to call our new function.
|
||||
|
||||
```
|
||||
void debugger::handle_command(const std::string& line) {
|
||||
auto args = split(line,' ');
|
||||
auto command = args[0];
|
||||
|
||||
if (is_prefix(command, "cont")) {
|
||||
continue_execution();
|
||||
}
|
||||
else if(is_prefix(command, "break")) {
|
||||
std::string addr {args[1], 2}; //naively assume that the user has written 0xADDRESS
|
||||
set_breakpoint_at_address(std::stol(addr, 0, 16));
|
||||
}
|
||||
else {
|
||||
std::cerr << "Unknown command\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
I’ve simply removed the first two characters of the string and called `std::stol` on the result, but feel free to make the parsing more robust. `std::stol` optionally takes a radix to convert from, which is handy for reading in hexadecimal.
|
||||
|
||||
* * *
|
||||
|
||||
### Continuing from the breakpoint
|
||||
|
||||
If you try this out, you might notice that if you continue from the breakpoint, nothing happens. That’s because the breakpoint is still set in memory, so it’s just hit repeatedly. The simple solution is to just disable the breakpoint, single step, re-enable it, then continue. Unfortunately we’d also need to modify the program counter to point back before the breakpoint, so we’ll leave this until the next post where we’ll learn about manipulating registers.
|
||||
|
||||
* * *
|
||||
|
||||
### Testing it out
|
||||
|
||||
Of course, setting a breakpoint on some address isn’t very useful if you don’t know what address to set it at. In the future we’ll be adding the ability to set breakpoints on function names or source code lines, but for now, we can work it out manually.
|
||||
|
||||
A simple way to test out your debugger is to write a hello world program which writes to `std::cerr` (to avoid buffering) and set a breakpoint on the call to the output operator. If you continue the debugee then hopefully the execution will stop without printing anything. You can then restart the debugger and set a breakpoint just after the call, and you should see the message being printed successfully.
|
||||
|
||||
One way to find the address is to use `objdump`. If you open up a shell and execute `objdump -d <your program>`, then you should see the disassembly for your code. You should then be able to find the `main` function and locate the `call` instruction which you want to set the breakpoint on. For example, I built a hello world example, disassembled it, and got this as the disassembly for `main`:
|
||||
|
||||
```
|
||||
0000000000400936 <main>:
|
||||
400936: 55 push %rbp
|
||||
400937: 48 89 e5 mov %rsp,%rbp
|
||||
40093a: be 35 0a 40 00 mov $0x400a35,%esi
|
||||
40093f: bf 60 10 60 00 mov $0x601060,%edi
|
||||
400944: e8 d7 fe ff ff callq 400820 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
|
||||
400949: b8 00 00 00 00 mov $0x0,%eax
|
||||
40094e: 5d pop %rbp
|
||||
40094f: c3 retq
|
||||
```
|
||||
|
||||
As you can see, we would want to set a breakpoint on `0x400944`to see no output, and `0x400949` to see the output.
|
||||
|
||||
* * *
|
||||
|
||||
### Finishing up
|
||||
|
||||
You should now have a debugger which can launch a program and allow the user to set breakpoints on memory addresses. Next time we’ll add the ability to read from and write to memory and registers. Again, let me know in the comments if you have any issues.
|
||||
|
||||
You can find the code for this post [here][3].
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: http://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
|
||||
|
||||
作者:[Simon Brand ][a]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]:http://blog.tartanllama.xyz/
|
||||
[1]:http://blog.tartanllama.xyz/c++/2017/03/21/writing-a-linux-debugger-setup/
|
||||
[2]:http://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
|
||||
[3]:https://github.com/TartanLlama/minidbg/tree/tut_break
|
@ -1,4 +1,4 @@
|
||||
开发 Linux 调试器第一部分:起步
|
||||
开发 Linux 调试器第一部分:启动
|
||||
============================================================
|
||||
|
||||
任何写过 hello world 程序的人都应该使用过调试器(如果你还没有,那就停下手头的工作先学习一下吧)。但是,尽管这些工具已经得到了广泛的使用,却并没有太多的资源告诉你它们的工作原理以及如何开发[1][1],尤其是和其它类似编译器等工具链技术相比的时候。
|
||||
@ -211,7 +211,7 @@ void debugger::continue_execution() {
|
||||
|
||||
* * *
|
||||
|
||||
### 完成
|
||||
### 总结
|
||||
|
||||
现在你应该编译一些 C 或者 C++ 程序,然后用你的调试器运行它们,看它是否能在函数入口暂停、从调试器中继续执行。在下一篇文章中,我们会学习如何让我们的调试器设置断点。如果你遇到了任何问题,在下面的评论框中告诉我吧!
|
||||
|
||||
|
@ -0,0 +1,202 @@
|
||||
开发 Linux 调试器第二部分:断点
|
||||
============================================================
|
||||
|
||||
在该系列的第一部分,我们写了一个小的进程启动器作为我们调试器的基础。在这篇博客中,我们会学习断点如何在 x86 Linux 上工作以及给我们工具添加设置断点的能力。
|
||||
|
||||
* * *
|
||||
|
||||
###系列文章索引
|
||||
|
||||
随着后面文章的发布,这些链接会逐渐生效。
|
||||
|
||||
1. [启动][2]
|
||||
2. [断点][3]
|
||||
3. 寄存器和内存
|
||||
4. Elves 和 dwarves
|
||||
5. 逐步、源码和信号
|
||||
6. Stepping on dwarves
|
||||
7. 源码层断点
|
||||
8. 调用栈
|
||||
9. 读取变量
|
||||
10. 下一步
|
||||
|
||||
* * *
|
||||
|
||||
### 断点如何形成?
|
||||
|
||||
有两种类型的断点:硬件和软件。硬件断点通常涉及设置和体系结构相关的寄存器来为你产生断点,而软件断点则涉及修改正在执行的代码。在这篇文章中我们只会关注软件断点,因为它们比较简单,而且足够完成你想要的功能。在 x86 机器上任一时刻你最多只能有 4 个硬件断点,但是它们能使你通过读取或者写入给定地址生效,而不是只有当执行到那里的时候。
|
||||
|
||||
我前面说软件断点是通过修改正在执行的代码实现的,那么问题就来了:
|
||||
|
||||
* 我们如何修改代码?
|
||||
* 为了设置断点我们要做什么修改?
|
||||
* 如何告知调试器?
|
||||
|
||||
第一个问题的答案显然是 `ptrace`。我们之前已经用它来启动我们的程序以便跟踪和继续它的执行,但我们也可以用它来读或者写内存。
|
||||
|
||||
当执行到断点时,我们的更改要让处理器暂停并给程序发送信号。在 x86 机器上这是通过 `int 3` 重写该地址上的指令实现的。x86 机器有个`interrupt vector table`(中断向量表),操作系统能用它来为多种事件注册处理程序,例如页故障、保护故障和无效操作码。它就像是注册错误处理回调函数,但是在硬件层面的。当处理器执行 `int 3` 指令时,控制权就被传递给断点中断处理器,对于 Linux 来说,就是给进程发送 `SIGTRAP` 信号。你可以在下图中看到这个进程,我们用 `0xcc`-`int 3` 的指令编码 - 覆盖了 `mov` 指令的第一个字节。
|
||||
|
||||
![断点](http://blog.tartanllama.xyz/assets/breakpoint.png)
|
||||
|
||||
最后一个谜题是调试器如何被告知中断的。如果你回顾前面的文章,我们可以用 `waitpid` 来监听被发送给被调试程序的信号。这里我们也可以这样做:设置断点、继续执行程序、调用 `waitpid` 然后等待直到发生 `SIGTRAP`。然后就可以通过打印已运行到的源码位置、或改变有图形用户界面的调试器中关注的代码行从而将这个断点传达给用户。
|
||||
|
||||
* * *
|
||||
|
||||
### 实现软件断点
|
||||
|
||||
我们会实现一个 `breakpoint` 类来表示某个位置的断点,我们可以根据需要启用或者停用该断点。
|
||||
|
||||
```
|
||||
class breakpoint {
|
||||
public:
|
||||
breakpoint(pid_t pid, std::intptr_t addr)
|
||||
: m_pid{pid}, m_addr{addr}, m_enabled{false}, m_saved_data{}
|
||||
{}
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
|
||||
auto is_enabled() const -> bool { return m_enabled; }
|
||||
auto get_address() const -> std::intptr_t { return m_addr; }
|
||||
|
||||
private:
|
||||
pid_t m_pid;
|
||||
std::intptr_t m_addr;
|
||||
bool m_enabled;
|
||||
uint64_t m_saved_data; //data which used to be at the breakpoint address
|
||||
};
|
||||
```
|
||||
这里的大部分代码都是跟踪状态;真正神奇的地方是 `enable` 和 `disable` 函数。
|
||||
|
||||
正如我们上面学到的,我们要用 `int 3` 指令 - 编码为 `0xcc` - 替换当前指定地址的指令。我们还要保存该地址之前的值,以便后面恢复代码;我们不想忘了执行用户的代码。
|
||||
|
||||
```
|
||||
void breakpoint::enable() {
|
||||
m_saved_data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);
|
||||
uint64_t int3 = 0xcc;
|
||||
uint64_t data_with_int3 = ((m_saved_data & ~0xff) | int3); //set bottom byte to 0xcc
|
||||
ptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3);
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
```
|
||||
|
||||
`ptrace` 的 `PTRACE_PEEKDATA` 请求完成如何读取被跟踪进程的内存。我们给它一个进程 ID 和一个地址,然后它返回给我们该地址当前的 64 位内容。 `(m_saved_data & ~0xff)` 把这个数据的低位字节置零,然后我们用它和我们的 `int 3` 指令按位或 `OR` 来设置断点。然后我们通过 `PTRACE_POKEDATA` 用我们的新数据覆盖那部分内存来设置断点。
|
||||
|
||||
`disable` 的实现比较简单,我们只需要恢复用 `0xcc` 覆盖的原始数据。
|
||||
|
||||
```
|
||||
void breakpoint::disable() {
|
||||
ptrace(PTRACE_POKEDATA, m_pid, m_addr, m_saved_data);
|
||||
m_enabled = false;
|
||||
}
|
||||
```
|
||||
|
||||
* * *
|
||||
|
||||
### 在调试器中增加断点
|
||||
|
||||
为了支持通过用户界面设置断点,我们要在 debugger 类修改三个地方:
|
||||
|
||||
1. 给 `debugger` 添加断点存储数据结构
|
||||
2. 添加 `set_breakpoint_at_address` 函数
|
||||
3. 给我们的 `handle_command` 函数添加 `break` 命令
|
||||
|
||||
我会将我的断点保存到 `std::unordered_map<std::intptr_t, breakpoint>` 结构,以便能简单快速地判断一个给定的地址是否有断点,如果有的话,取回该 breakpoint 对象。
|
||||
|
||||
```
|
||||
class debugger {
|
||||
//...
|
||||
void set_breakpoint_at_address(std::intptr_t addr);
|
||||
//...
|
||||
private:
|
||||
//...
|
||||
std::unordered_map<std::intptr_t,breakpoint> m_breakpoints;
|
||||
}
|
||||
```
|
||||
|
||||
在 `set_breakpoint_at_address` 函数中我们会新建一个 breakpoint 对象、启用它、把它添加到数据结构里、并给用户打印一条信息。如果你喜欢的话,你可以重构所有的输出信息,从而你可以将调试器作为库或者命令行工具使用,为了简便,我把它们都整合到了一起。
|
||||
|
||||
```
|
||||
void debugger::set_breakpoint_at_address(std::intptr_t addr) {
|
||||
std::cout << "Set breakpoint at address 0x" << std::hex << addr << std::endl;
|
||||
breakpoint bp {m_pid, addr};
|
||||
bp.enable();
|
||||
m_breakpoints[addr] = bp;
|
||||
}
|
||||
```
|
||||
|
||||
现在我们会在我们的命令处理程序中增加对我们新函数的调用。
|
||||
|
||||
```
|
||||
void debugger::handle_command(const std::string& line) {
|
||||
auto args = split(line,' ');
|
||||
auto command = args[0];
|
||||
|
||||
if (is_prefix(command, "cont")) {
|
||||
continue_execution();
|
||||
}
|
||||
else if(is_prefix(command, "break")) {
|
||||
std::string addr {args[1], 2}; //naively assume that the user has written 0xADDRESS
|
||||
set_breakpoint_at_address(std::stol(addr, 0, 16));
|
||||
}
|
||||
else {
|
||||
std::cerr << "Unknown command\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我删除了字符串中的前两个字符并对结果调用 `std::stol`,为了让解析更加强壮,你也可以修改它。`std::stol` 可以将字符串按照所给基数转化为整数。
|
||||
|
||||
* * *
|
||||
|
||||
### 从断点继续执行
|
||||
|
||||
如果你尝试这样做,你可能会发现如果你从断点处继续执行,不会发生任何事情。这是因为断点仍然在内存中,因此一直被命中。简单的解决办法就是停用这个断点、运行到下一步、再次启用这个断点、然后继续执行。不幸的是我们还需要更改程序计数器指回断点前面,我们会将这部分内容留到下一篇博客学习了如何操作寄存器之后。
|
||||
|
||||
* * *
|
||||
|
||||
### 测试它
|
||||
|
||||
当然,如果你不知道要设置的地址,设置断点并非很有帮助。后面我们会学习如何在函数名或者代码行设置断点,但现在我们可以通过手动实现。
|
||||
|
||||
测试你调试器的简单方法是写一个 hello world 程序,这个程序输出到 `std::err`(为了避免缓存),并在调用输出操作符的地方设置断点。如果你继续执行被调试的程序,执行很可能会停止而不会输出任何东西。然后你可以重启调试器并在调用之后设置一个断点,现在你应该看到成功地输出了消息。
|
||||
|
||||
查找地址的一个方法是使用 `objdump`。如果你打开一个终端并执行 `objdump -d <your program>`,然后你应该看到你程序的反汇编代码。然后你就可以找到 `main` 函数并定位到你想设置断点的 `call` 指令。例如,我编译了一个 hello world 程序,反汇编它,然后得到了 `main` 的反汇编代码:
|
||||
|
||||
```
|
||||
0000000000400936 <main>:
|
||||
400936: 55 push %rbp
|
||||
400937: 48 89 e5 mov %rsp,%rbp
|
||||
40093a: be 35 0a 40 00 mov $0x400a35,%esi
|
||||
40093f: bf 60 10 60 00 mov $0x601060,%edi
|
||||
400944: e8 d7 fe ff ff callq 400820 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
|
||||
400949: b8 00 00 00 00 mov $0x0,%eax
|
||||
40094e: 5d pop %rbp
|
||||
40094f: c3 retq
|
||||
```
|
||||
|
||||
正如你看到的,要没有输出,我们要在 `0x400944` 设置断点,要看到输出,要在 `0x400949` 设置断点。
|
||||
|
||||
* * *
|
||||
|
||||
### 总结
|
||||
|
||||
现在你应该有了一个可以启动程序、允许在内存地址上设置断点的调试器。后面我们会添加读写内存和寄存器的功能。再次说明,如果你有任何问题请在评论框中告诉我。
|
||||
|
||||
你可以在[这里][3] 找到该项目的代码。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: http://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
|
||||
|
||||
作者:[Simon Brand ][a]
|
||||
译者:[ictlyh](https://github.com/ictlyh)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]:http://blog.tartanllama.xyz/
|
||||
[1]:http://blog.tartanllama.xyz/c++/2017/03/21/writing-a-linux-debugger-setup/
|
||||
[2]:http://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
|
||||
[3]:https://github.com/TartanLlama/minidbg/tree/tut_break
|
Loading…
Reference in New Issue
Block a user