-10
2022-08-17T09:21:18+00:00
http://hushi55.github.io
DNS txt 记录查询
2022-07-12T00:00:00+00:00
http://hushi55.github.io/2022/07/12/dns
<p>在配置 dns 的记录时候,有时候需要本地验证是不是配置争取,比如验证 txt 记录是否配置等。</p>
<h2 id="验证-txt-记录">验证 txt 记录</h2>
<p>一般情况我们使用 <code class="language-plaintext highlighter-rouge">dig</code> 或者 <code class="language-plaintext highlighter-rouge">nslookup</code> 两个命令来验证</p>
<h3 id="dig">dig</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dig google.com TXT
</code></pre></div></div>
<p>输出如下</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; <<>> DiG 9.10.6 <<>> google.com TXT
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53727
;; flags: qr rd ra; QUERY: 1, ANSWER: 11, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;google.com. IN TXT
;; ANSWER SECTION:
google.com. 3600 IN TXT "v=spf1 include:_spf.google.com ~all"
google.com. 3600 IN TXT "atlassian-domain-verification=5YjTmWmjI92ewqkx2oXmBaD60Td9zWon9r6eakvHX6B77zzkFQto8PQ9QsKnbf4I"
google.com. 3600 IN TXT "google-site-verification=TV9-DBe4R80X4v0M4U_bd_J9cpOJM0nikft0jAgjmsQ"
google.com. 3600 IN TXT "webexdomainverification.8YX6G=6e6922db-e3e6-4a36-904e-a805c28087fa"
google.com. 3600 IN TXT "facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95"
google.com. 3600 IN TXT "globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8="
google.com. 3600 IN TXT "google-site-verification=wD8N7i1JTNTkezJ49swvWW48f8_9xveREV4oB-0Hf5o"
google.com. 3600 IN TXT "apple-domain-verification=30afIBcvSuDV2PLX"
google.com. 3600 IN TXT "docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"
google.com. 3600 IN TXT "MS=E4A68B9AB2BB9670BCE15412F62916164C0B20BB"
google.com. 3600 IN TXT "docusign=1b0a6754-49b1-4db5-8540-d2c12664b289"
;; Query time: 78 msec
;; SERVER: 30.30.30.30#53(30.30.30.30)
;; WHEN: Tue Jul 12 14:37:12 CST 2022
;; MSG SIZE rcvd: 811
</code></pre></div></div>
<h3 id="nsloopup">nsloopup</h3>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># nslookup</span>
<span class="o">></span> server google-public-dns-a.google.com
<span class="o">></span> <span class="nb">set type</span><span class="o">=</span>txt
<span class="o">></span> google.com
</code></pre></div></div>
<p>输出</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>;; Truncated, retrying in TCP mode.
Server: google-public-dns-a.google.com
Address: 8.8.8.8#53
Non-authoritative answer:
google.com text = "MS=E4A68B9AB2BB9670BCE15412F62916164C0B20BB"
google.com text = "facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95"
google.com text = "docusign=1b0a6754-49b1-4db5-8540-d2c12664b289"
google.com text = "google-site-verification=wD8N7i1JTNTkezJ49swvWW48f8_9xveREV4oB-0Hf5o"
google.com text = "webexdomainverification.8YX6G=6e6922db-e3e6-4a36-904e-a805c28087fa"
google.com text = "docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"
google.com text = "apple-domain-verification=30afIBcvSuDV2PLX"
google.com text = "google-site-verification=TV9-DBe4R80X4v0M4U_bd_J9cpOJM0nikft0jAgjmsQ"
google.com text = "globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8="
google.com text = "v=spf1 include:_spf.google.com ~all"
google.com text = "atlassian-domain-verification=5YjTmWmjI92ewqkx2oXmBaD60Td9zWon9r6eakvHX6B77zzkFQto8PQ9QsKnbf4I"
Authoritative answers can be found from:
>
</code></pre></div></div>
<h2 id="参考">参考</h2>
<ul>
<li><a href="http://jaroker.com/technotes/security/email/email-troubleshooting/check-dns-txtrecords/?lang=zh">检查DNS TXT记录</a></li>
</ul>
IT's story
2021-11-26T00:00:00+00:00
http://hushi55.github.io/2021/11/26/IT-story
<p><img src="/images/other/20190604233510897.png" alt="" /></p>
<p>这篇 blog 主要讲述听到,看到的各种有意思的 IT 领域的小故事,
其实美藏在世界的各个角落,这些故事里基本上都隐藏着工程师的优雅。</p>
<h2 id="linux-文件格式与魔兽">linux 文件格式与魔兽</h2>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/286088470" title="ELF">ELF</a>:Excutable and Linking Format 的缩写,是 linux 下二进制文件、可执行文件、目标代码、共享库和core转存格式文件。</li>
<li><a href="https://zhuanlan.zhihu.com/p/302726082" title="DWARF">DWARF</a>:Debugging With Attributed Record Formats 的缩写,是 linux 下调试信息的存储文件格式</li>
<li><a href="https://zhuanlan.zhihu.com/p/302726082" title="ORC">ORC</a>:Oops Rewind Capability 的缩写,用于栈回溯跟踪的一种文件格式。</li>
</ul>
<p>其中这三个名称分别对应魔兽中的</p>
<ul>
<li>ELF:精灵</li>
<li>DWARF:矮人</li>
<li>ORC:兽人</li>
</ul>
<h2 id="ibm-和-sum-中的故事">IBM 和 SUM 中的故事</h2>
<p>当年 SUM 公司开发的 JAVA 语言风靡一时,SUM 的市值也如日中天,最高的时候超过了 2000 亿美金。
IBM 当年觊觎 SUM 公司的 JAVA 语言,就在 JAVA 语言的开发工具起了 Eclipse 的名称。
eclipse 的中文意思是 <code class="language-plaintext highlighter-rouge">日食</code>,而拥有 JAVA 语言公司的名称是 SUM 中文意思 <code class="language-plaintext highlighter-rouge">太阳</code>。
从这命名中也可以看出 IBM 的野心。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/286088470">ELF</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/302726082">DWARF</a></li>
</ul>
golang ebpf issues
2021-11-24T00:00:00+00:00
http://hushi55.github.io/2021/11/24/golang-ebpf-issue
<p>将 ebpf 技术使用到 golang 程序中时,通常会遇到一些问题,这篇 blog 主要是收集这些问题和一些解决方案</p>
<h2 id="go-crash-with-uretprobe">Go crash with uretprobe</h2>
<p>在 golang 程序中使用 uretprobe 有可能会出现程序奔溃到情况,这还不是最恶劣的情况,
如果程序没有奔溃也有可能出现程序带着脏数据执行,出现完全不可控到情况,
具体 <a href="https://github.com/iovisor/bcc/issues/1320">issue</a> 可以参考</p>
<h3 id="why-为什么会出现这种问题">why? 为什么会出现这种问题</h3>
<p>golang 出现这种情况最主要到原因是 golang 特定的 goroutine 协程机制,
因为 goroutine 的原因,每个 goroutine 会有自己独立的 stack 空间,最开始的时候分配的大小为 2k,
但是如果 goroutine 的栈空间超过了 2k 这时 golang 程序会重新分配一个 4k 的栈空间,并且将之前 stack
上的数据 copy 到新的 stack 上,这就是 golang 的连续栈分配方式。详细的介绍可以参考 <a href="https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.5.html" title="连续栈">连续栈</a> 这篇文章。</p>
<p>其次由于 uretprobe 机制,这个可以参考我前面的<a href="/2021/05/19/trampoline-introduction">文章</a>
从这篇文章可以知道,uretprobe 探针类型会使用 trampoline 蹦床机制,通过 JMP 指令插入自定义逻辑,
但是最后需要 JMP 回原地址,结合 golang 的 <a href="https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.5.html" title="连续栈">连续栈</a> 机制,如果 goroutine 在调用过程发生了栈扩容/收缩
都会导致 JMP 回原地址是错误的,所以会导致 golang 程序崩溃或者不可遇知的错误。</p>
<h3 id="how-怎么解决这个问题">how? 怎么解决这个问题</h3>
<p>我们知道 golang 在 uretprobe 探针奔溃的原因后,是不是 golang 程序就无法获取函数的返回值呢?
在这个 <a href="https://github.com/iovisor/bcc/issues/1320#issuecomment-407927542">issue</a> 中,
提供了一个不依赖 uretprobe 获取返回值的解决方案。</p>
<ul>
<li>不使用 uretprobe 探针</li>
<li>扫描源程序 ELF 文件,找到函数的开始位置,在所有 RET 指令位置注入 uprobe 探针,这样可以模拟 uretprobe</li>
</ul>
<p>这个方案可以解决上述问题,并且还有一些性能优势,因为避免了 uretprobe 的开销</p>
<ul>
<li>正常的函数调用: 2 ns/call</li>
<li>使用 uretprobe 函数调用 : 4 us/call</li>
<li>使用 2 uprobes (at enter + RET instructions): 3 us/call</li>
</ul>
<p>上述数据使用简单的 libc 循环调用测试。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://github.com/iovisor/bcc/issues/1320#issuecomment-407927542">Go crash with uretprobe</a></li>
</ul>
go calling convention
2021-05-20T00:00:00+00:00
http://hushi55.github.io/2021/05/20/go-calling-convention-x86-64
<p>这篇文章翻译自 <code class="language-plaintext highlighter-rouge">dr knz @ work</code> go 调用规约,原文出处在文章末尾,作者有两篇文章。
由于最近需要对各种语言对内存布局感兴趣,所以打算先研究 go 语言的调用规约,翻译可能存在错误,欢迎斧正。</p>
<h2 id="introduction">Introduction</h2>
<h2 id="calling-convention">Calling convention</h2>
<h3 id="arguments-and-return-value">Arguments and return value</h3>
<h3 id="call-sequence-how-a-function-gets-called">Call sequence: how a function gets called</h3>
<p>在 go 1.10 和 1.15 版本中,
一个函数为被调用函数设置好参数,并且在栈上预留出返回值的空间,被调用函数在返回值写入预留的空间中。</p>
<p>在以前一样,这样设计的一个副作用是,当函数返回一个</p>
<p>As before, a side effect of this design is that when a function returns the same value as one of its callees,
it needs to read the return value from the callee from its own activation record,
then place it back onto the stack at a return value in its caller’s activation record.
Tail call optimizations (TCO) thus remain impossible.</p>
<p>Additionally, function prologues remain largely unchanged:</p>
<ul>
<li>一个函数使用本地变量,需要通过 SP 寄存器相对位置来获取,这个总是出现在前置序言中。</li>
<li>和以前一样,每一个函数会设置一个帧指针 BP 寄存器,来方便异常释放。</li>
<li>和以前一样,一个函数需要使用更多的栈空间时,这样的情况下需要检查当前栈上的剩余空间,如果不足就需要分配更多的空间。
这是因为 go 默认为 goroutines 分配很小的栈空间。</li>
</ul>
<p>通产,结束不会做这些操作。</p>
<p>这里又一个例子来自 go 运行时的内部函数,来演示函数序言和结束:</p>
<pre><code class="language-cgo">internal/cpu.Initialize:
; Check remaining stack size:
MOVQ FS:0xfffffff8, CX
CMPQ 0x10(CX), SP ; at least 24 bytes on the stack?
JBE 0x401047 ; no: go to block at end of function below
; Allocate activation record:
SUBQ $0x18, SP ; 24 bytes in activation record
; Set up the frame pointer
MOVQ BP, 0x10(SP) ; BP is callee-save: store it
LEAQ 0x10(SP), BP ; set up new frame pointer
...
MOVQ 0x10(SP), BP ; restore the caller's frame pointer
ADDQ $0x18, SP ; deallocate the activation record
RET ; return
0x401047:
CALL runtime.morestack_noctxt(SB) ; alloc more stack
JMP internal/cpu.Initialize(SB) ; restart
</code></pre>
<h3 id="callee-save-registersor-not">Callee-save registers—or not</h3>
<h3 id="the-cost-of-pointers-and-interfaces">The cost of pointers and interfaces</h3>
<pre><code class="language-cgo">// Define a struct type implementing the interface by value.
type bar struct{ x int }
func (bar) foo() {}
// Define a global variable so we don't use the heap allocator.
var y bar
// Make an interface value.
func MakeInterface2() Foo { return y }
</code></pre>
<pre><code class="language-cgo">MakeInterface2:
; <function prologue>
; write y to 0(SP), as an argument to runtime.convT64
0x45c55d 488b057cc70900 MOVQ main.y(SB), AX
0x45c564 48890424 MOVQ AX, 0(SP)
; call runtime.convT64, this converts the object to a heap reference
0x45c568 e833c5faff CALL runtime.convT64(SB)
; extract the return value
0x45c56d 488b442408 MOVQ 0x8(SP), AX
; take the vtable pointer
0x45c572 488d0d07c00200 LEAQ go.itab.main.bar,main.Foo(SB), CX
; write both to the return value slot for MakeInterface2
0x45c579 48894c2420 MOVQ CX, 0x20(SP)
0x45c57e 4889442428 MOVQ AX, 0x28(SP)
; <function epilogue>
0x45c58c c3 RET
</code></pre>
<pre><code class="language-cgo">MakeInterface2:
; <function prologue>
; take the vtable pointer
0x4805dd 488d053c020400 LEAQ go.itab.src.bar,src.Foo(SB), AX
; pass it as argument to convT2I64
0x4805e4 48890424 MOVQ AX, 0(SP)
; take the address of y
0x4805e8 488d05e9f10b00 LEAQ main.y(SB), AX
; pass it as argument to convT2I64
0x4805ef 4889442408 MOVQ AX, 0x8(SP)
; convert to interface reference
0x4805f4 e8e7b2f8ff CALL runtime.convT2I64(SB)
; copy the return value from runtime.convT2I64 to the return slot of MakeInterface2
0x4805f9 488b442410 MOVQ 0x10(SP), AX
0x4805fe 488b4c2418 MOVQ 0x18(SP), CX
0x480603 4889442430 MOVQ AX, 0x30(SP)
0x480608 48894c2438 MOVQ CX, 0x38(SP)
; <function epilogue>
0x480616 c3 RET
</code></pre>
<h3 id="vararg-calls">Vararg calls</h3>
<pre><code class="language-cgo">func f(...int) {}
var x,y,z,w int
func caller() {
f(x,y,z,w)
}
</code></pre>
<pre><code class="language-cgo">caller:
; <function prologue>
; fill the slice:
XORPS X0, X0 ; set 2 words (128 bit) to zero in X0
MOVUPS X0, 0x18(SP) ; initialize the 4-element slice to zero
MOVUPS X0, 0x28(SP) ; initialize the 4-element slice to zero
MOVQ main.x(SB), AX
MOVQ AX, 0x18(SP) ; store x into 1st position
MOVQ main.y(SB), AX
MOVQ AX, 0x20(SP) ; store y into 2nd position
MOVQ main.z(SB), AX
MOVQ AX, 0x28(SP) ; store z into 3rd position
MOVQ main.w(SB), AX
MOVQ AX, 0x30(SP) ; store w into 4th position
; prepare the slice as outgoing argument:
LEAQ 0x18(SP), AX ; store the base address
MOVQ AX, 0(SP)
MOVQ $0x4, 0x8(SP) ; store the length
MOVQ $0x4, 0x10(SP) ; store the capacity
CALL main.g(SB) ; call the function
; <function epilogue>
RET
</code></pre>
<pre><code class="language-cgo">// note: now we have an interface type.
func f(...interface{}) {}
var x,y,z,w int
func caller() {
f(x,y,z,w)
}
</code></pre>
<pre><code class="language-cgo">caller:
; <function prologue>
; fill the slice:
XORPS X0, X0 ; zero out the slice
MOVUPS X0, 0x38(SP)
MOVUPS X0, 0x48(SP)
MOVUPS X0, 0x58(SP)
MOVUPS X0, 0x68(SP)
MOVQ main.x(SB), AX
MOVQ AX, 0x30(SP) ; copy x on the stack, out of the slice
LEAQ 0x7995(IP), AX
MOVQ AX, 0x38(SP) ; place x's interface{} vtable ptr in the slice
LEAQ 0x30(SP), CX
MOVQ CX, 0x40(SP) ; place the address of x's copy in the slice
MOVQ main.y(SB), CX
MOVQ CX, 0x28(SP) ; copy y on the stack, out of the slice
MOVQ AX, 0x48(SP) ; place the same vtable ptr as x in the slice
LEAQ 0x28(SP), CX
MOVQ CX, 0x50(SP) ; place the address of y's copy in the slice
MOVQ main.z(SB), CX
MOVQ CX, 0x20(SP) ; copy z on the stack, out of the slice
MOVQ AX, 0x58(SP) ; place the same vtable ptr as x in the slice
LEAQ 0x20(SP), CX
MOVQ CX, 0x60(SP) ; place the address of z's copy in the slice
MOVQ main.w(SB), CX ; copy w on the stack, out of the slice
MOVQ CX, 0x18(SP)
MOVQ AX, 0x68(SP) ; place the same vtable ptr as x in the slice
LEAQ 0x18(SP), AX
MOVQ AX, 0x70(SP) ; place the address of w's copy in the slice
LEAQ 0x38(SP), AX
MOVQ AX, 0(SP) ; set the slice base address as argument
MOVQ $0x4, 0x8(SP) ; the slice's size
MOVQ $0x4, 0x10(SP) ; the slice's capacity
CALL main.g(SB) ; call the function
; <function epilogue>
RET
</code></pre>
<h2 id="exception-handling">Exception handling</h2>
<h3 id="implementation-of-defer">Implementation of <code class="language-plaintext highlighter-rouge">defer</code></h3>
<pre><code class="language-cgo">func Defer1() int { defer f(); return 123 }
</code></pre>
<pre><code class="language-cgo">Defer1:
; <function prologue>
0x45c4bd MOVQ $0x0, AX
0x45c4c4 MOVQ AX, 0x8(SP) ; set up a word full with zeroes
0x45c4c9 MOVB $0x0, 0x7(SP) ; set the first byte to zero (redundant)
; write zero to the return value slot
0x45c4ce MOVQ $0x0, 0x20(SP)
; defer the call to f()
0x45c4d7 LEAQ 0x1b672(IP), AX
0x45c4de MOVQ AX, 0x8(SP) ; write the address of f
0x45c4e3 MOVB $0x1, 0x7(SP) ; let the runtime know there is 1 defer
; write the return value 123
0x45c4e8 MOVQ $0x7b, 0x20(SP)
; un-defer
0x45c4f1 MOVB $0x0, 0x7(SP) ; let the runtime know there is no more defer
; final call to f() on the return path
0x45c4f6 CALL main.f(SB)
; <function epilogue>
0x45c504 RET
; the following code is called during unwinds after a recover,
; not on the common case:
0x45c505 CALL runtime.deferreturn(SB)
0x45c50a MOVQ 0x10(SP), BP
0x45c50f ADDQ $0x18, SP
0x45c513 RET
</code></pre>
<h3 id="implementation-of-panic">Implementation of <code class="language-plaintext highlighter-rouge">panic</code></h3>
<pre><code class="language-cgo">func Panic() { panic(123) }
</code></pre>
<pre><code class="language-cgo">Panic:
; <function prologue>
; load the vtable for interface{}:
LEAQ 0x78dc(IP), AX
MOVQ AX, 0(SP)
; load the address of a static copy of the
; integer value 123:
LEAQ 0x2afa9(IP), AX
MOVQ AX, 0x8(SP)
; call gopanic:
CALL runtime.gopanic(SB)
NOPL
; note: function epilogue omitted in this case
</code></pre>
<h3 id="catching-exceptions-defer--recover">Catching exceptions: <code class="language-plaintext highlighter-rouge">defer</code> + <code class="language-plaintext highlighter-rouge">recover</code></h3>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://dr-knz.net/go-calling-convention-x86-64.html">go calling convention 1</a></li>
<li><a href="https://dr-knz.net/go-calling-convention-x86-64-2020.html">go calling convention 2</a></li>
<li><a href="https://go.googlesource.com/proposal/+/refs/changes/78/248178/1/design/40724-register-calling.md">Register-based Go calling convention</a></li>
</ul>
trampoline introduction
2021-05-19T00:00:00+00:00
http://hushi55.github.io/2021/05/19/trampoline-introduction
<p>trampoline 在接触 ebpf 技术后,我们会经常遇到的一个技术术语,对于这个技术,
我在网上查找了一些技术资料,尝试介绍这个技术,如果不对,欢迎斧正。</p>
<h2 id="changing-the-control-flow-in-the-system-call-handler">Changing the Control Flow in the System Call Handler</h2>
<p>到目前为止,我们找到两个合适到为止来劫持系统的控制。
它们都出现在 kernel 2.6 版本中,因此有着非常好的可移植性和可用性。</p>
<p><img src="/images/linux/trampoline-impl.png" alt="" /></p>
<p>我们将通过无条件跳入 trampoline 来重写代码的选定部分。
trampoline 有几个工作。</p>
<ol>
<li>首先得保持栈,确保可以方便的访问系统调用的结果。</li>
<li>随后,trampoline 会调用 hijack() 函数,这个会修改系统调用的结果(例如:ls 命令删除隐藏目录的记录)</li>
<li>当 hijack() 执行完毕,返回 trampoline,需要恢复系统的状态到调用之前
<ul>
<li>清理栈</li>
<li>补偿在进入 trampoline 后的指令</li>
</ul>
</li>
</ol>
<h2 id="总结">总结</h2>
<ol>
<li>替换的指令长度应该等于被替换的指令长度</li>
<li>在 trampoline 中需要 push 参数到 hijack</li>
<li>hijack 函数调用完毕后需要清理 push 到参数</li>
<li>补偿执行被替换到指令</li>
<li>恢复跳转到被替换指令之后</li>
</ol>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://core.ac.uk/download/pdf/62916134.pdf">Hijacking the Linux Kernel</a></li>
<li><a href="https://lwn.net/Articles/804937/">Introduce BPF trampoline</a></li>
</ul>
User-Space Probes
2021-05-18T00:00:00+00:00
http://hushi55.github.io/2021/05/18/User-Space-Probes
<p>由于最近在研究 ebpf ,对于 ebpf 和 trace 相关的实现原理希望有些了解。
这篇文章介绍 uprobes 的相关概念和实现原理。
这篇文章翻译自 <code class="language-plaintext highlighter-rouge">User-Space Probes (Uprobes)</code>
作者 <code class="language-plaintext highlighter-rouge">Jim Keniston</code> 原文链接在文末。</p>
<h2 id="concepts-uprobes-return-probes">Concepts: Uprobes, Return Probes</h2>
<p>Uprobes 能够动态的非破坏性的插入任务程序,收集应用程序的 debugging 和 性能信息。
我们可以在代码任何位置,在命中断点时,kernel 对应的处理函数将会被调用。</p>
<p>当前存在两个类型的 user-space 探针:</p>
<ul>
<li>uprobes</li>
<li>uretprobes(也就是返回类型探针)</li>
</ul>
<p>一个探针可以插入指令在一个应用程序的虚拟地址空间中。
返回类型探针触发时机是在用户指定的函数返回时。</p>
<p>注册函数 <code class="language-plaintext highlighter-rouge">register_uprobe()</code> 能够注册探针到进程中。
当探针被插入,当探针命中时,对应的处理函数将被调用。</p>
<p>通常,uprobes 是一组 kernel 模块。在一个最简单的案例中,
模块的 init 函数安装一个或者多个探针,exit 函数注销探针的注册。
当然,探针也可以注册和注销在其他事件中,例如:</p>
<ul>
<li>探针可以注册和注销其他探针</li>
<li>我们可以建立 utrace 回调中注册和注销探针,当 进程 forks 了进程,clones 了线程,
execs 进入一个系统调用,收到一个 signal。</li>
</ul>
<h3 id="how-does-a-uprobe-work">How Does a Uprobe Work?</h3>
<p>当一个探针被注册,uprobes 将拷贝一个被探针指令的副本,挂起被探针的进程,
使用断点指令(在 i386 和 x86_64 是 int3)替换掉这个被探针的第一个指令,
随后运行被挂起的进程。(当插入断点时,uprobes 使用类似 ptrace 的 copy-on-write 机制,
这样使得这个断点只会影响当前这个进程,其他进程正常运行,即使探针在共享库上)</p>
<p>当 CPU 命中这个断点探针时,trap 将发生,CPU 将保存 user-mode 下的寄存器,
并且生成一个 SIGTRAP 信号。uprobes 拦截到 SIGTRAP 信号,并且找到相关的 uprobe。
随后执行这个 uprobe 关联的处理函数,传递 uprobe 结构体的地址和保存的寄存器给这个处理函数。
这个处理函数可能回阻塞,但是我们仍然只会挂在被探针的线程。</p>
<p>随即,uprobes 将在上面拷贝的副本指令下单步执行,唤醒挂起的进程在探针点出继续执行。
(实际上单步执行原始指令会更简单,但之后,Uprobes 必须移除断点指令。
这在多线程应用程序中会引起问题。比如,当另一个线程执行过探测点的时会打开一个时间窗口。)</p>
<p>存储单步执行指令的区域在每一个进程的 SSOL 区域,这个是一个被 uprobes 创建的很小的虚拟地址空间。</p>
<h3 id="the-role-of-utrace">The Role of Utrace</h3>
<p>当一个探针注册到之前没有被探针到进程中时,
uprobes 将使用 utrace 为进程中到每个线程创建一个跟踪引擎。
uprobes 使用 utrace 的<code class="language-plaintext highlighter-rouge">静默</code>机制在插入和移除断点之前停止所有的线程。
utrace 也会在断点,单步调试陷入和其他有趣的事情上通知 uprobes,
例如:fork,clone,exec,exit。</p>
<h3 id="how-does-a-return-probe-work">How Does a Return Probe Work?</h3>
<p>当调用 register_uretprobe() 后,uprobes 将在函数到入口出建立一个探针。
当函数被调用,这个探针将被命中,uprobes 保存这个函数当返回地址当一个副本,
使用 trampoline 地址替换调这个返回地址,这段代码中包含了一个断点指令。</p>
<p>当探针函数执行返回指令时,断点命中,控制将转移到 trampoline 上。
探针的 trampoline 处理函数将调用 uretprobe 指定当用户处理函数,
设置这个 PC 指针到保存的返回地址上,当 trap 返回这是执行将恢复。</p>
<p>trampoline 被存储在 SSOL 区域。</p>
<h3 id="multithreaded-applications">Multithreaded Applications</h3>
<p>uprobes 支持在多线程应用程序上使用探针。
uprobes 不会限制被探针进程的线程树。
所有进程中的线程都有同样的 <code class="language-plaintext highlighter-rouge">text page</code>,所有每一个探针将影响进程中的所有线程。
当然每一个线程命中 probepoint 是单独的。多个线程可能同是运行同一个处理函数。
如果我们想部分线程或者特定的一组线程运行处理函数,
我们只能通过检测当前的线程 ID 来决定是不是命中 probepoint。</p>
<p>当一个进程 clones 一个新的线程,这个线程将自动的拥有这个进程的所有探针。</p>
<p>当我们注册/注销一个探针时,断点不会被插入和删除,一直到 utrace 停止进程中的所有线程。
注册/注销 函数在断点已经被 插入/删除 后返回。</p>
<h3 id="registering-probes-within-probe-handlers">Registering Probes within Probe Handlers</h3>
<h2 id="architectures-supported">Architectures Supported</h2>
<p>uprobes 和 uretprobes 当前支持的架构有:</p>
<ul>
<li>i386</li>
<li>x86_64 (AMD-64, EM64T)</li>
<li>ppc64</li>
<li>ia64</li>
<li>s390x</li>
</ul>
<h2 id="interoperation-with-kprobes">Interoperation with Kprobes</h2>
<p>Uprobes 打算与 Kprobes 进行有效的相互操作。
例如,检测模块可以同时调用 Kprobes API 和 Uprobes API。</p>
<p>uprobe 或 uretprobe 回调函数可以注册或注销 kprobes、jprobes、kretprobes,以及 uprobes 和 uretprobes。
另外,kprobe、jprobe、kretprobe 回调函数一定不能休眠,不然会无法注册或注销这些探针。</p>
<p>注意,<code class="language-plaintext highlighter-rouge">u[ret]probe</code> 类型的探针性能开支会比 <code class="language-plaintext highlighter-rouge">k[ret]probe</code> 类型高出好几倍。</p>
<h2 id="interoperation-with-utrace">Interoperation with Utrace</h2>
<p>在上面的介绍中,我们知道 uprobes 是 utrace 的一个 client。
对于每一个可探针的线程,Uprobes 会创建一个 Utrace 引擎,
随后为以下时间注册回调:clone/fork,exec,exit,”core-dump” 信号(包括断点陷阱)。
当进程第一触发探针时,Uprobes 创建这个引擎,或者 Uprobes 通知线程创建,这两个都是可以的。</p>
<h2 id="probe-overhead">Probe Overhead</h2>
<p>2007 中的一个通常 CPU,一个探针大概会有 3 微秒的延迟。
特别的,在同一个探针点,触发一个简单的打印时间基准测试中。
一秒的 qps 是 300000 - 350000 之间,不同的架构有所不同。
一个返回类型探针通常会比 uprobe 探针多大概 50% 。
当一个函数已经有一个返回类型探针,这是添加一个 uprobe 探针将不会有额外的新能损失。</p>
<p>这里不同架构下的样列:</p>
<ul>
<li>u = uprobe</li>
<li>r = return probe</li>
<li>ur = uprobe + return probe</li>
</ul>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>i386: Intel Pentium M, 1495 MHz, 2957.31 bogomips
u = 2.9 usec; r = 4.7 usec; ur = 4.7 usec
x86_64: AMD Opteron 246, 1994 MHz, 3971.48 bogomips
// TODO
ppc64: POWER5 (gr), 1656 MHz (SMT disabled, 1 virtual CPU per physical CPU)
// TODO
</code></pre></div></div>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://sourceware.org/git/?p=systemtap.git;a=blob_plain;f=runtime/uprobes/uprobes.txt;hb=a2b182f549cf64427a474803f3c02286b8c1b5e1">Uprobes</a></li>
<li><a href="https://jayce.github.io/public/posts/trace/user-space-probes/">译|2008|User-Space Probes (Uprobes)</a></li>
</ul>
Process scheduler
2021-05-14T00:00:00+00:00
http://hushi55.github.io/2021/05/14/linux-kernel-process-scheduler
<h2 id="process-lifetime">Process lifetime</h2>
<p>进程的生命周期如下图所示:</p>
<p><img src="http://myaut.github.io/dtrace-stap-book/images/sched.png" alt="" /></p>
<p>在调度器中,上下文切换分为以下两个步骤:</p>
<ul>
<li>当前任务离开 CPU,以下是引起这个的原因:
<ul>
<li>task 被 kernel 同步对象阻塞<code class="language-plaintext highlighter-rouge">(6)</code>,例如调用 poll() 等待网络数据。
在这种情况下,task 会被放置到同步对象的 <code class="language-plaintext highlighter-rouge">sleep queue</code> 中。
如果这个随后被其他 task 解除阻塞<code class="language-plaintext highlighter-rouge">(7)</code>,这个会被移动到 <code class="language-plaintext highlighter-rouge">run-queue</code> 中。</li>
<li>task 通过调用 <code class="language-plaintext highlighter-rouge">yield()</code> 主动放弃 CPU 的控制<code class="language-plaintext highlighter-rouge">(3) </code>。</li>
<li>task CPU 时间片到期,或者有更高优先级的 task 添加到 run queue 中,这个被叫做<code class="language-plaintext highlighter-rouge">抢占</code>。
<code class="language-plaintext highlighter-rouge">(3)</code>一些系统调用和中断也可以触发上下文切换。</li>
</ul>
</li>
<li>新的 task 被挑选到 CPU 上运行<code class="language-plaintext highlighter-rouge">(2)</code>。</li>
</ul>
<p>在 SMP 系统中,操作系统至少会为每个 CPU 创建一个 run-queue。
当一些 CPU 空闲时,这个会检查其他 CPU 的 run-queues 并且 steal task,
因此 task 会在 CPU 间迁移(5)。这个能平衡不同 CPU 间的 task,
但是其他因素也是需要考量的,如 NUMA 进程内存的局部性,缓存热度等。</p>
<p>如前所述,调度器的主要目标是分配 task 的执行时间。为了做到这个,
为了 task 的优先,所以会创建多个 queues 选择一个 task 来执行(每一个优先级一个队列)。
而不是遍历所有的队列选择高优先级的 task 执行。这种方式被叫做 cyclic planning。
Fair planning,会考虑不同的线程,进程,用户,服务,尝试平衡处理器上的运行时间。</p>
<p>通常 task 会阻塞在各种同步对象上等待可用数据,例如 <code class="language-plaintext highlighter-rouge">accept()</code> 会一直阻塞直到 client 端 connect,
<code class="language-plaintext highlighter-rouge">recv()</code> 将一直阻塞直到 client 端发送新的数据。当前没有可用数据时是不需要 CPU 的,
所以 task 简单的放弃 CPU,并且放置到相关对象特定的 sleep queue 中。
也就是说 <code class="language-plaintext highlighter-rouge">accept()</code> 调用,linux kernel <code class="language-plaintext highlighter-rouge">socket</code> 对象的 <code class="language-plaintext highlighter-rouge">sk_wq</code> 等待队列上。</p>
<h2 id="scheduling-in-linux">Scheduling in Linux</h2>
<p>让我们来看看 CFS 调度器的实现细节。
首先,调度器不会直接处理 task,调度器以 <code class="language-plaintext highlighter-rouge">struct sched_entity</code> 为调度单元。
调度单元可以代表一个 task,或者一组以队列存在的 task,数据结构为 <code class="language-plaintext highlighter-rouge">struct cfs_rq</code>(通过属性 <code class="language-plaintext highlighter-rouge">my_q</code> 引用),
所有允许构建有层级的调度单元,分配资源给一组 task 这种技术叫做 <code class="language-plaintext highlighter-rouge">CGroups</code>。
<code class="language-plaintext highlighter-rouge">struct rq</code> 中的属性 cfs 这个是 <code class="language-plaintext highlighter-rouge">struct cfs_rq</code> 的一个实例,包含了所有高优先级的调度单元,这个是处理器的运行队列。
每一个调度单元有一个 <code class="language-plaintext highlighter-rouge">cfs_rq</code> 指针,这个指向 CFS 的运行队列:</p>
<p><img src="http://myaut.github.io/dtrace-stap-book/images/linux/sched.png" alt="" /></p>
<p>我们举例来说明,运行队列有两个调度单元:
一个是 CFS 队列包含单个 task(这个通过父指针引用 cfs_rq),一个是 top-level 基本的 task。</p>
<p>CFS 不会像 Solaris 系统中的 TS 调度器一样分配时间片。
使用 task 在 CPU 上的总运行时间来代替,这个时间保存在 <code class="language-plaintext highlighter-rouge">sum_exec_runtime</code> 属性上。
当调度 task 调度到 CPU 上时,<code class="language-plaintext highlighter-rouge">sum_exec_runtime</code> 会保存到 <code class="language-plaintext highlighter-rouge">prev_sum_exec_runtime</code> 上,
所以计算这个两个属性的差值,就能直到这个 task 在 CPU 上的运行时间。
<code class="language-plaintext highlighter-rouge">sum_exec_runtime</code> 的计时单位是纳秒,但是这个不会直接用来度量 task 的运行时间。
为了实现抢占式,CFS 使用运行时长除以 task 权重(属性 <code class="language-plaintext highlighter-rouge">load.weight</code>),
所以更高权重的 task 将会有更低的度量(保存在 <code class="language-plaintext highlighter-rouge">vruntime</code> 属性中)。
根据 task 的 <code class="language-plaintext highlighter-rouge">vruntime</code> 排序在红黑树中,这个红黑树被叫做 <code class="language-plaintext highlighter-rouge">tasks_timeline</code>,
所有最左边的 task 有最低的 <code class="language-plaintext highlighter-rouge">vruntime</code>,这个保存在红黑树的 <code class="language-plaintext highlighter-rouge">rb_leftmost</code>。</p>
<p>CFS 对于已经被换醒的 task 存在特殊的例子。
因为这个 task 已经 sleep 太久,所以它们的 vruntime 可能会太低,它们会存在不公平的 CPU 时间。
为了防止这种情况,CFS 会保留一个最小的 vruntime 在 task 属性 min_vruntime 中,
所有唤醒的 task 将使用 min_vruntime 减去预计的 timeslice 值。
CFS 有个伙伴系统,帮助调度程序指向:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 下一个最近唤醒的 task
- 最后一个被驱逐的并且跳过通过调用 sched_yield() 的 task
</code></pre></div></div>
<h2 id="参考">参考</h2>
<ul>
<li><a href="http://myaut.github.io/dtrace-stap-book/kernel/sched.html">Process scheduler</a></li>
</ul>
Process management
2021-05-13T00:00:00+00:00
http://hushi55.github.io/2021/05/13/linux-kernel-process-management
<h2 id="process-tree-in-linux">Process tree in Linux</h2>
<p>进程和线程的实现是通过通用数据结构 <code class="language-plaintext highlighter-rouge">task_struct</code>(<code class="language-plaintext highlighter-rouge">include/linux/sched.h</code>),
进程中的第一个线程被叫做 task 组 leader,其他线程通过 thread_node 节点来连接。
通过 task_struct 来引用,也就是 task_struct 是 task 组 leader。
子进程通过 parent 指针来引用父进程,通过 sibling 来链接其他节点。
父进程通过 children 来链接子节点。</p>
<p>task_struct 对象中的关系如下图:</p>
<p><img src="http://myaut.github.io/dtrace-stap-book/images/linux/task.png" alt="" /></p>
<p>task_struct 一些重要的属性:</p>
<ul>
<li>mm(指向 struct mm_struct) 引用这个进程的地址空间。例如:exe_file(指向 struct file) 引用可执行文件,
arg_start 和 arg_end 是传递给进程的 argv 参数第一和最后一个字节</li>
<li>fs(指向 struct fs_struct) 包含文件系统相关信息:path 指 task 的工作目录,root 指 root 目录(可以通过系统调用 chroot 改变)</li>
<li>start_time 和 real_start_time 进程的实际运行时间</li>
<li>files(指向 struct files_struc) 包含了进程打开的文件</li>
<li>utime 和 stime 用户态和 kernel 态在 CPU 上的运行时间</li>
</ul>
<h2 id="lifetime-of-a-process">Lifetime of a process</h2>
<p>下图是一个进程的生命周期和相应的探针点:</p>
<p><img src="http://myaut.github.io/dtrace-stap-book/images/forkproc.png" alt="" /></p>
<p>Unix 进程生成分为两个阶段:</p>
<ul>
<li>父进程调用 <code class="language-plaintext highlighter-rouge">fork()</code> 系统调用。kernel 创建一个父进程的副本,
包括地址空间(在 <code class="language-plaintext highlighter-rouge">copy-on-write</code> 模式下),打开的文件,分配一个新的 PID。
如果 <code class="language-plaintext highlighter-rouge">fork()</code> 调用成功,这个将返回在两个进程的上下文中,这个有同一个指令指针(PC 指针是一样的)
在子进程中随后的代码通常用来关闭文件,重置信号等。</li>
<li>子进程调用 <code class="language-plaintext highlighter-rouge">execve()</code> 系统调用,这个将使用新的 based 传递给 execve() 来替换掉进程的地址空间。</li>
</ul>
<p>当调用 <code class="language-plaintext highlighter-rouge">exit()</code> 系统调用,子进程将结束。
但是,进程也可以会被 <code class="language-plaintext highlighter-rouge">killed</code>,当 kernel 出现不正确的条件(引发 kernel oops) 或者机器错误。
如果父进程像等待子进程结束,这个可以调用 <code class="language-plaintext highlighter-rouge">wait()</code> 系统调用(或者 <code class="language-plaintext highlighter-rouge">waitid()</code>),
<code class="language-plaintext highlighter-rouge">wait()</code> 调用将收到进程的退出码,随后关联的 <code class="language-plaintext highlighter-rouge">task_struct</code> 将被销毁。
如果父进程不像等待子进程,子进程退出后,这个将被作为僵尸进程。
父进程可能会收到 kernel 发送的 <code class="language-plaintext highlighter-rouge">SIGCHLD</code> 信号。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="http://myaut.github.io/dtrace-stap-book/kernel/proc.html">Process management</a></li>
</ul>