├── .gitignore ├── LICENSE ├── README.md ├── example └── webapp │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── router │ └── handler.go │ └── run_webapp.sh ├── go.mod ├── go.sum ├── gostw.go ├── gostw21.go ├── gostw22.go ├── gostw23.go ├── hotfix.go ├── logo.png ├── patcher.go └── picker.go /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go template 2 | # If you prefer the allow list template instead of the deny list, see community template: 3 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 4 | # 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright © 2024-present go-hotfix-projects 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | go-hotfix 3 |

hotfix

4 |

hotfix is a golang function hot-fix solution

5 |
6 |
7 | 8 | 9 | > 警告: 目前尚未经过严格测试,请勿用于生产环境 10 | > Warning: This has not been rigorously tested, do not use in production environment 11 | 12 | > 注意: 不支持Windows 13 | > Note: Windows is not supported 14 | 15 | > 注意: 为了确保函数都能被修补,需要关闭函数内联,会因此损失一些性能 16 | > Note: To ensure that all functions can be patched, function inlining needs to be disabled, which will result in a loss of some performance 17 | 18 | ## Features 19 | * 支持指定包级别/类级别/函数级别热补丁支持 20 | * Supported for hot patching at package/class/function 21 | * 支持导出函数/私有函数/成员方法修补 22 | * Support for exporting functions/private functions/member methods patching 23 | * 基于[monkey-patch](https://github.com/brahma-adshonor/gohook) + [plugin](https://pkg.go.dev/plugin)机制实现 24 | * Implemented based on [monkey-patch](https://github.com/brahma-adshonor/gohook) + [plugin](https://pkg.go.dev/plugin) 25 | * 线程安全, 使用 `stw` 确保所有协程都进入安全点从而实现线程安全的补丁 26 | * Thread safety, use `stw` to ensure that all coroutines enter safe points to hot patching 27 | 28 | ## Limits 29 | * 受限于[go plugin](https://pkg.go.dev/plugin)仅支持Linux, FreeBSD, macOS,其他平台目前不支持 30 | * The go plugin is currently only supported on Linux, FreeBSD, and macOS platforms; other platforms are not supported 31 | * 加载的补丁包无法卸载,如果热修复次数过多可能导致较大的内存占用 32 | * The loaded patch package cannot be uninstalled. Too many hot fixes may result in large memory usage 33 | * 不支持对闭包进行修复,需要热修复的逻辑不要放在闭包中 34 | * Closures are not supported, and logic that requires hotfixes should not be placed in closures 35 | * 不能修改已有数据结构和函数签名,否则可能将导致程序崩溃,应该仅用于bug修复 36 | * Cannot modify existing data structures and function signatures, as this may lead to program crashes. It should only be used for bug fixes 37 | * 编译时请保留调试符号,并且禁用函数内联`-gcflags=all=-l -N` 38 | * Please keep the debugging symbols when compiling, and disable function inline `-gcflags=all=-l -N` 39 | * 编译错误`invalid reference to xxxx` 是因为 `go1.23`开始限制了`go:linkname`功能,必须添加编译参数关闭限制`-ldflags=-checklinkname=0` 40 | * The compilation error `invalid reference to xxxx` is because `go1.23` began to limit the `go:linkname` function, and the compilation parameter must be added to turn off the restriction `-ldflags=-checklinkname=0` 41 | * 补丁包的的编译环境必须和主程序一致,包括go编译器版本,编译参数,依赖等,否则加载补丁包将会失败 42 | * The patch package's build environment must match that of the main program, including the Go compiler version, compilation parameters, dependencies, etc., otherwise loading the patch package will fail 43 | * 补丁包的`main`包下面的`init`会首先调用一次,请注意不要重复初始化 44 | * The 'init' under the 'main' package of the patch package will be called once first, be careful not to initialize it repeatedly 45 | * 打补丁包时`main`包必须产生变化,否则可能出现 `plugin already loaded`错误,推荐使用 `-ldflags="-X main.HotfixVersion=v1.0.1"`指定版本号, 确保每次编译补丁包都会有变化 46 | * The `main` package must be changed when applying the patch package, otherwise the `plugin already loaded` error may occur. It is recommended to use `-ldflags="-X main.HotfixVersion=v1.0.1"` to specify the version number to ensure that the patch package is compiled every time There will be changes 47 | * **该方案处于实验性质,尚未经过严格验证** 48 | * **The solution is experimental in nature and has not yet been rigorously validated** 49 | 50 | ## Example 51 | 52 | 参考这个[例子](./example/webapp)项目 53 | Refer to this [example](./example/webapp) project 54 | 55 | 56 | ## Acknowledgments 57 | This project inspired by lsg2020/go-hotfix 58 | 59 | ## License 60 | 61 | The repository released under version 2.0 of the Apache License. -------------------------------------------------------------------------------- /example/webapp/go.mod: -------------------------------------------------------------------------------- 1 | module webapp 2 | 3 | go 1.23.0 4 | 5 | replace github.com/go-hotfix/hotfix => ../../ 6 | 7 | require github.com/go-hotfix/hotfix v0.0.0-00010101000000-000000000000 8 | 9 | require ( 10 | github.com/brahma-adshonor/gohook v1.1.9 // indirect 11 | github.com/cilium/ebpf v0.11.0 // indirect 12 | github.com/go-delve/delve v1.24.2 // indirect 13 | github.com/go-hotfix/assembly v0.0.0-20250411022734-bc10e3192e90 // indirect 14 | github.com/hashicorp/golang-lru v1.0.2 // indirect 15 | golang.org/x/arch v0.11.0 // indirect 16 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect 17 | golang.org/x/sys v0.32.0 // indirect 18 | golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /example/webapp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/brahma-adshonor/gohook v1.1.9 h1:YuQFj8rhAj1kvtGHUc5BlLvAELw98M/ydpBz/MvBXNU= 2 | github.com/brahma-adshonor/gohook v1.1.9/go.mod h1:3B9f7Lwh7z5fW6MvUvGxCFaVdloVI+1tOsl4BMJdWr0= 3 | github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= 4 | github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 9 | github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 10 | github.com/go-delve/delve v1.24.2 h1:BPuAHfgM8fAzomRuo02S2YRA6OEvY7gB0aK8DcHzbZY= 11 | github.com/go-delve/delve v1.24.2/go.mod h1:kJk12wo6PqzWknTP6M+Pg3/CrNhFMZvNq1iHESKkhv8= 12 | github.com/go-hotfix/assembly v0.0.0-20250411022734-bc10e3192e90 h1:U61b3L2UDgTf06331xSisLHJjyVtYaQ0x7pSNOWAV3A= 13 | github.com/go-hotfix/assembly v0.0.0-20250411022734-bc10e3192e90/go.mod h1:Q4JWNLFccz4MDFisgt1xM0jgVIvKnV2EHvOiTP/zOZc= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 17 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 18 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 19 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 23 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 27 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 30 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 31 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 33 | golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= 34 | golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 35 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= 36 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 37 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 38 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 39 | golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 h1:TCDqnvbBsFapViksHcHySl/sW4+rTGNIAoJJesHRuMM= 40 | golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5/go.mod h1:8nZWdGp9pq73ZI//QJyckMQab3yq7hoWi7SI0UIusVI= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 44 | -------------------------------------------------------------------------------- /example/webapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-hotfix/hotfix" 8 | "webapp/router" 9 | ) 10 | 11 | var HotfixVersion string 12 | 13 | func main() { 14 | http.HandleFunc("/now", router.TimeHandler) 15 | http.HandleFunc("/hotfix", HotfixHandler) 16 | 17 | http.ListenAndServe(":8080", nil) 18 | } 19 | 20 | func HotfixHandler(w http.ResponseWriter, r *http.Request) { 21 | 22 | fmt.Printf("apply patching...\n") 23 | 24 | res := hotfix.Hotfix("webapp_v1.so", hotfix.Package("webapp/router")) 25 | 26 | fmt.Fprintln(w, fmt.Sprintf("patch: %s, cost: %s ", res.Patch, res.Cost.String())) 27 | if nil != res.Err { 28 | fmt.Fprintln(w, "patch failed: ", res.Err) 29 | } 30 | fmt.Fprintln(w, "methods:") 31 | for i, name := range res.Methods { 32 | fmt.Fprintln(w, "\t", i, ": ", name) 33 | } 34 | fmt.Fprintln(w, "logs:") 35 | fmt.Fprintln(w, res.Message) 36 | } 37 | -------------------------------------------------------------------------------- /example/webapp/router/handler.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | func TimeHandler(w http.ResponseWriter, r *http.Request) { 9 | // 在编译脚本提示 `please modify v1 plugin ...` 时切换下列代码注释,模拟业务函数功能变更 10 | // Toggle the following code comments when the script prompts 'please modify v1 plugin ...' to simulate the function change 11 | // 12 | var nowTimeStr = time.Now().Format(time.DateTime) 13 | //var nowTimeStr = time.Now().Format(time.RFC1123) 14 | w.Write([]byte(nowTimeStr + "\n")) 15 | } 16 | -------------------------------------------------------------------------------- /example/webapp/run_webapp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | GO_VERSION=$(go version | awk '{print $3}' | cut -c 3-) 6 | MIN_VERSION="1.23" 7 | 8 | if [ "$(printf "$GO_VERSION\n$MIN_VERSION" | sort -V | head -n1)" = "$MIN_VERSION" ]; then 9 | GO_BUILD_LDFLAGS="-checklinkname=0" 10 | else 11 | GO_BUILD_LDFLAGS="" 12 | fi 13 | 14 | echo "build webapp..." 15 | go build -gcflags="all=-l -N" -ldflags="-X main.HotfixVersion=main ${GO_BUILD_LDFLAGS}" -o webapp . 16 | 17 | echo "please modify v1 plugin, press enter key to continue..." 18 | read input 19 | 20 | echo "build webapp plugin v1..." 21 | go build -gcflags="all=-l -N" -buildmode=plugin -ldflags="-X main.HotfixVersion=v1 ${GO_BUILD_LDFLAGS}" -o webapp_v1.so . 22 | 23 | echo "run main program..." 24 | ./webapp & 25 | 26 | # wait webapp to start 27 | sleep 2s 28 | 29 | echo "(before): get server response..." 30 | curl http://127.0.0.1:8080/now 31 | 32 | echo "(hotfix): start hotfix...." 33 | curl http://127.0.0.1:8080/hotfix 34 | 35 | echo "(after): get server response..." 36 | curl http://127.0.0.1:8080/now 37 | 38 | # kill webapp 39 | pkill webapp -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-hotfix/hotfix 2 | 3 | go 1.23.0 4 | 5 | //replace github.com/go-hotfix/assembly => ../assembly 6 | 7 | require ( 8 | github.com/brahma-adshonor/gohook v1.1.9 9 | github.com/go-delve/delve v1.24.2 10 | github.com/go-hotfix/assembly v0.0.0-20250411022734-bc10e3192e90 11 | ) 12 | 13 | require ( 14 | github.com/cilium/ebpf v0.11.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/hashicorp/golang-lru v1.0.2 // indirect 17 | github.com/stretchr/testify v1.7.0 // indirect 18 | golang.org/x/arch v0.11.0 // indirect 19 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect 20 | golang.org/x/sys v0.32.0 // indirect 21 | golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/brahma-adshonor/gohook v1.1.9 h1:YuQFj8rhAj1kvtGHUc5BlLvAELw98M/ydpBz/MvBXNU= 2 | github.com/brahma-adshonor/gohook v1.1.9/go.mod h1:3B9f7Lwh7z5fW6MvUvGxCFaVdloVI+1tOsl4BMJdWr0= 3 | github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= 4 | github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 9 | github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 10 | github.com/go-delve/delve v1.24.2 h1:BPuAHfgM8fAzomRuo02S2YRA6OEvY7gB0aK8DcHzbZY= 11 | github.com/go-delve/delve v1.24.2/go.mod h1:kJk12wo6PqzWknTP6M+Pg3/CrNhFMZvNq1iHESKkhv8= 12 | github.com/go-hotfix/assembly v0.0.0-20250411022734-bc10e3192e90 h1:U61b3L2UDgTf06331xSisLHJjyVtYaQ0x7pSNOWAV3A= 13 | github.com/go-hotfix/assembly v0.0.0-20250411022734-bc10e3192e90/go.mod h1:Q4JWNLFccz4MDFisgt1xM0jgVIvKnV2EHvOiTP/zOZc= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 16 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 17 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 18 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 19 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 23 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 27 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 30 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 31 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 33 | golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= 34 | golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 35 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= 36 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 37 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 38 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 39 | golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5 h1:TCDqnvbBsFapViksHcHySl/sW4+rTGNIAoJJesHRuMM= 40 | golang.org/x/telemetry v0.0.0-20241106142447-58a1122356f5/go.mod h1:8nZWdGp9pq73ZI//QJyckMQab3yq7hoWi7SI0UIusVI= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 46 | -------------------------------------------------------------------------------- /gostw.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.21 2 | 3 | package hotfix 4 | 5 | import _ "unsafe" 6 | 7 | //go:linkname _stopTheWorld runtime.stopTheWorld 8 | func _stopTheWorld(reason string) 9 | 10 | //go:linkname startTheWorld runtime.startTheWorld 11 | func startTheWorld() 12 | 13 | //go:nosplit 14 | func stopTheWorld() { 15 | _stopTheWorld("hot-patching") 16 | } 17 | -------------------------------------------------------------------------------- /gostw21.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 && !go1.22 2 | 3 | package hotfix 4 | 5 | import _ "unsafe" 6 | 7 | //go:linkname _stopTheWorld runtime.stopTheWorld 8 | func _stopTheWorld(reason uint8) 9 | 10 | //go:linkname startTheWorld runtime.startTheWorld 11 | func startTheWorld() 12 | 13 | //go:nosplit 14 | func stopTheWorld() { 15 | // stwUnknown stwReason = iota // "unknown" 16 | _stopTheWorld(0) 17 | } 18 | -------------------------------------------------------------------------------- /gostw22.go: -------------------------------------------------------------------------------- 1 | //go:build go1.22 && !go1.23 2 | 3 | package hotfix 4 | 5 | import _ "unsafe" 6 | 7 | // stwReason is an enumeration of reasons the world is stopping. 8 | type stwReason uint8 9 | 10 | // worldStop provides context from the stop-the-world required by the 11 | // start-the-world. 12 | type worldStop struct { 13 | reason stwReason 14 | start int64 15 | } 16 | 17 | var _stopFlag worldStop 18 | 19 | //go:linkname _stopTheWorld runtime.stopTheWorld 20 | func _stopTheWorld(reason stwReason) worldStop 21 | 22 | //go:linkname _startTheWorld runtime.startTheWorld 23 | func _startTheWorld(w worldStop) 24 | 25 | //go:nosplit 26 | func startTheWorld() { 27 | _startTheWorld(_stopFlag) 28 | _stopFlag = worldStop{} 29 | } 30 | 31 | //go:nosplit 32 | func stopTheWorld() { 33 | // stwUnknown stwReason = iota // "unknown" 34 | _stopFlag = _stopTheWorld(0) 35 | } 36 | -------------------------------------------------------------------------------- /gostw23.go: -------------------------------------------------------------------------------- 1 | //go:build go1.23 2 | 3 | package hotfix 4 | 5 | import _ "unsafe" 6 | 7 | // stwReason is an enumeration of reasons the world is stopping. 8 | type stwReason uint8 9 | 10 | // worldStop provides context from the stop-the-world required by the 11 | // start-the-world. 12 | type worldStop struct { 13 | reason stwReason 14 | startedStopping int64 15 | finishedStopping int64 16 | stoppingCPUTime int64 17 | } 18 | 19 | var _stopFlag worldStop 20 | 21 | //go:linkname _stopTheWorld runtime.stopTheWorld 22 | func _stopTheWorld(reason stwReason) worldStop 23 | 24 | //go:linkname _startTheWorld runtime.startTheWorld 25 | func _startTheWorld(w worldStop) 26 | 27 | //go:nosplit 28 | func startTheWorld() { 29 | _startTheWorld(_stopFlag) 30 | _stopFlag = worldStop{} 31 | } 32 | 33 | //go:nosplit 34 | func stopTheWorld() { 35 | // stwUnknown stwReason = iota // "unknown" 36 | _stopFlag = _stopTheWorld(0) 37 | } 38 | -------------------------------------------------------------------------------- /hotfix.go: -------------------------------------------------------------------------------- 1 | package hotfix 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "plugin" 8 | "reflect" 9 | "runtime" 10 | "runtime/debug" 11 | "sort" 12 | "strings" 13 | "sync/atomic" 14 | "time" 15 | "unsafe" 16 | 17 | "github.com/go-delve/delve/pkg/proc" 18 | "github.com/go-hotfix/assembly" 19 | ) 20 | 21 | var exclusivity int32 22 | 23 | type Request struct { 24 | Logger *log.Logger // Debug logger. 25 | Patch string // Plugin file. 26 | ThreadSafe bool // Whether it is thread safe. 27 | Methods []string // Patching function list. 28 | Assembly assembly.DwarfAssembly // Go runtime assembly. 29 | OldFuncEntrys []*proc.Function // Old function entrys. 30 | OldFunctions []reflect.Value // Old function values. 31 | NewFunctions []reflect.Value // Plugin function values. 32 | } 33 | 34 | type Result struct { 35 | Assembly assembly.DwarfAssembly 36 | Patch string // Plugin file 37 | ThreadSafe bool // Whether it is thread safe, the default is false, use stw mechanism to ensure thread safety. 38 | Methods []string // Patching function list. 39 | Cost time.Duration // Total of cost time. 40 | Err error // Patching failed reason. 41 | Message string // Patching debug message. 42 | } 43 | 44 | // Hotfix Apply hot patching by default. 45 | func Hotfix(libPath string, funcPicker FuncPicker, threadSafe ...bool) Result { 46 | return DoHotfix(libPath, funcPicker, GoMonkey(), threadSafe...) 47 | } 48 | 49 | // DoHotfix Apply hot patching in a custom way. 50 | func DoHotfix(libPath string, funcPicker FuncPicker, funcPatcher FuncPatcher, threadSafe ...bool) (result Result) { 51 | 52 | var start = time.Now() 53 | var funcNames []string 54 | var returnErr error 55 | var output bytes.Buffer 56 | var logger = log.New(&output, "[hotfix]", log.LstdFlags|log.Lshortfile) 57 | 58 | defer func() { 59 | rr := recover() 60 | 61 | result.Patch = libPath 62 | result.ThreadSafe = len(threadSafe) > 0 && threadSafe[0] 63 | result.Methods = funcNames 64 | result.Cost = time.Since(start) 65 | result.Message = strings.TrimSpace(output.String()) 66 | result.Err = returnErr 67 | if nil != rr { 68 | err, ok := rr.(error) 69 | if !ok { 70 | err = fmt.Errorf("%v", rr) 71 | } 72 | if returnErr == nil { 73 | result.Err = fmt.Errorf("%w\n%s", err, debug.Stack()) 74 | } else { 75 | result.Err = fmt.Errorf("%s: %w\n%s", returnErr.Error(), err, debug.Stack()) 76 | } 77 | } 78 | }() 79 | 80 | // 获取全局独占锁 81 | // 不允许并发执行热修复 82 | if !atomic.CompareAndSwapInt32(&exclusivity, 0, 1) { 83 | returnErr = fmt.Errorf("an other hotfix in processing") 84 | return 85 | } 86 | 87 | // 释放全局独占锁 88 | defer atomic.StoreInt32(&exclusivity, 0) 89 | 90 | logger.Printf("arch: %s/%s, compiler: %s/%s, cpu: %d-bit, jump code size: %d", runtime.GOOS, runtime.GOARCH, runtime.Compiler, runtime.Version(), archMode, jumpCodeSize) 91 | 92 | t0 := time.Now() 93 | 94 | // 加载主程序集 95 | logger.Printf("loading main assembly ...") 96 | if result.Assembly, returnErr = assembly.NewDwarfAssembly(); nil != returnErr { 97 | returnErr = fmt.Errorf("main assembly load failed: %w", returnErr) 98 | return 99 | } 100 | 101 | t1 := time.Now() 102 | 103 | logger.Printf("load main assembly finished, cost: %s", t1.Sub(t0).String()) 104 | 105 | // 输出当前已经加载的插件 106 | if plugins, addrs, err := result.Assembly.SearchPlugins(); nil == err { 107 | for i, plug := range plugins { 108 | if 0 != addrs[i] { 109 | logger.Printf("loaded dynamic library: %s@%#x", plug, addrs[i]) 110 | } 111 | } 112 | } 113 | 114 | // 加载需要热修复的函数 115 | logger.Printf("lookup patch functions ...") 116 | funcNames, returnErr = funcPicker(result.Assembly) 117 | if nil != returnErr { 118 | return 119 | } 120 | 121 | if 0 == len(funcNames) { 122 | returnErr = fmt.Errorf("empty functions") 123 | return 124 | } 125 | 126 | // 删除重复的项目 127 | funcNames = uniqStrings(funcNames) 128 | // 排序一下 129 | sort.Strings(funcNames) 130 | 131 | // 检查待热更的函数是否存在 132 | oldFuncEntrys := make([]*proc.Function, 0, len(funcNames)) 133 | for _, name := range funcNames { 134 | // 查找当前等待补丁的函数地址 135 | entry, err := result.Assembly.FindFuncEntry(name) 136 | if nil != err { 137 | returnErr = fmt.Errorf("%w: function not found: %s", err, name) 138 | return 139 | } 140 | 141 | logger.Printf("find function: %s, entry: %#x, codeSpace: %d", name, entry.Entry, entry.End-entry.Entry) 142 | 143 | // jump code 代码不能比原有的代码还长,否则将产生覆写,这里直接拒绝 144 | if size := entry.End - entry.Entry; size < jumpCodeSize { 145 | returnErr = fmt.Errorf("jump code overflow: %s, size: %d, required: %d", name, size, jumpCodeSize) 146 | return 147 | } 148 | 149 | oldFuncEntrys = append(oldFuncEntrys, entry) 150 | } 151 | 152 | t2 := time.Now() 153 | 154 | logger.Printf("lookup patch functions finished, cost: %s", t2.Sub(t1).String()) 155 | 156 | // 加载动态库到进程空间 157 | logger.Printf("opening patch %s ...", libPath) 158 | if _, err := plugin.Open(libPath); nil != err { 159 | returnErr = err 160 | return 161 | } 162 | 163 | // 查找的插件在主进程中的地址 164 | lib, addr, err := result.Assembly.SearchPluginByName(libPath) 165 | if nil != err { 166 | returnErr = fmt.Errorf("%w: plugin not found: %s", err, libPath) 167 | return 168 | } 169 | 170 | // 插件查找失败 171 | if "" == lib { 172 | returnErr = fmt.Errorf("search plugin image failed: %s", libPath) 173 | return 174 | } 175 | 176 | t3 := time.Now() 177 | 178 | logger.Printf("opening patch %s finished, cost: %s", lib, t3.Sub(t2).String()) 179 | 180 | // 使用完整路径 181 | libPath = lib 182 | 183 | // 加载插件符号表 184 | logger.Printf("load patch assembly ...") 185 | if err = result.Assembly.LoadImage(lib, addr); nil != err { 186 | returnErr = fmt.Errorf("%w: load plugin assembly failed: %s", err, lib) 187 | return 188 | } 189 | 190 | t4 := time.Now() 191 | logger.Printf("load patch assembly finished, cost: %s", t4.Sub(t3).String()) 192 | 193 | logger.Printf("validating hotfix functions ... ") 194 | 195 | newFunctions := make([]reflect.Value, 0, len(funcNames)) 196 | oldFunctions := make([]reflect.Value, 0, len(funcNames)) 197 | for i, name := range funcNames { 198 | // 查找插件补丁类型 199 | hotfixFunc, err := result.Assembly.FindFunc(name, false) 200 | if nil != err { 201 | returnErr = fmt.Errorf("validating failed: %w: function not found: %s", err, name) 202 | return 203 | } 204 | 205 | // 如果补丁中存在某个函数则在LoadAssembly中会被替换为新函数对象,函数地址会变更为补丁函数地址 206 | // 如果指定的函数补丁中不存在那么无法对这个函数进行修补 207 | if newEntry := hotfixFunc.Pointer(); newEntry == uintptr(oldFuncEntrys[i].Entry) { 208 | returnErr = fmt.Errorf("validating failed: function not found in patch: %s", name) 209 | return 210 | } 211 | 212 | logger.Printf("validating hotfix function: %s, entry: %#x -> %#x", name, oldFuncEntrys[i].Entry, hotfixFunc.Pointer()) 213 | 214 | newFunctions = append(newFunctions, hotfixFunc) 215 | 216 | // 统一旧函数对象类型(插件和主程序的类型不一样) 217 | oldFunc := assembly.CreateFuncForCodePtr(hotfixFunc.Type(), oldFuncEntrys[i].Entry) 218 | oldFunctions = append(oldFunctions, oldFunc) 219 | } 220 | 221 | t5 := time.Now() 222 | logger.Printf("validating hotfix functions ... finished, cost: %s", t5.Sub(t4).String()) 223 | 224 | // 执行补丁操作 225 | logger.Printf("apply patch ... patch: %s, threadSafe: %v", lib, len(threadSafe) > 0 && threadSafe[0]) 226 | returnErr = funcPatcher(Request{ 227 | Logger: logger, 228 | Patch: libPath, 229 | ThreadSafe: len(threadSafe) > 0 && threadSafe[0], 230 | Methods: funcNames, 231 | Assembly: result.Assembly, 232 | OldFuncEntrys: oldFuncEntrys, 233 | OldFunctions: oldFunctions, 234 | NewFunctions: newFunctions, 235 | }) 236 | 237 | t6 := time.Now() 238 | 239 | if nil != returnErr { 240 | logger.Printf("apply patch failed: %v, cost: %s", returnErr, t6.Sub(t5).String()) 241 | } else { 242 | logger.Printf("apply patch success, cost: %s", t6.Sub(t5).String()) 243 | } 244 | 245 | return 246 | } 247 | 248 | var archMode = 64 249 | 250 | func init() { 251 | sz := unsafe.Sizeof(uintptr(0)) 252 | if sz == 4 { 253 | archMode = 32 254 | } 255 | } 256 | 257 | // jumpCodeSize count jump code size 258 | var jumpCodeSize = uint64(len(genJumpCode(archMode, true, 0, 0))) 259 | 260 | //go:linkname genJumpCode github.com/brahma-adshonor/gohook.genJumpCode 261 | func genJumpCode(mode int, rdxIndirect bool, to, from uintptr) []byte 262 | 263 | func uniqStrings(collection []string) []string { 264 | result := make([]string, 0, len(collection)) 265 | seen := make(map[string]struct{}, len(collection)) 266 | 267 | for _, item := range collection { 268 | if _, ok := seen[item]; ok { 269 | continue 270 | } 271 | 272 | seen[item] = struct{}{} 273 | result = append(result, item) 274 | } 275 | 276 | return result 277 | } 278 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-hotfix/hotfix/c4b37e9699a86192f0239bb64e8a75ba051ee119/logo.png -------------------------------------------------------------------------------- /patcher.go: -------------------------------------------------------------------------------- 1 | package hotfix 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/brahma-adshonor/gohook" 7 | ) 8 | 9 | // FuncPatcher To apply function hot patching. 10 | type FuncPatcher func(req Request) error 11 | 12 | // GoMonkey Hot patching implementation based on monkey-patching. 13 | func GoMonkey() FuncPatcher { 14 | return func(req Request) error { 15 | // 代码热修复采用monkey-patch机制实现函数调用重定向(重写跳转指令) 16 | // 因为写跳转指令是非原子性的,因此在多线程环境无法保证安全的重写跳转指令 17 | // 需要一些方案确保能安全的重写跳转指令 18 | // 1. ptrace 使用外部程序模拟调试器行为(挂起程序,如果程序正在函数中则单步执行直到跳出函数调用范围) 19 | // 2. 程序内部保证(模拟类似safe-point机制) 20 | // 3. 参考runtime.GC使程序进入stw状态后重写跳转指令 21 | 22 | // 这里采用第三种方案,使程序进入stw装后进行补丁操作 23 | // 如果线程不安全则采用stw的方式确保补丁能安全执行,避免线程安全问题 24 | if !req.ThreadSafe { 25 | req.Logger.Printf("enter stw...") 26 | stopTheWorld() 27 | req.Logger.Printf("enter stw... finished") 28 | 29 | defer func() { 30 | req.Logger.Printf("leave stw...") 31 | startTheWorld() 32 | req.Logger.Printf("leave stw... finished") 33 | }() 34 | } 35 | 36 | req.Logger.Printf("monkey patching...") 37 | for i := 0; i < len(req.OldFunctions); i++ { 38 | if err := gohook.HookByIndirectJmp(req.OldFunctions[i].Interface(), req.NewFunctions[i].Interface(), nil); nil != err { 39 | return fmt.Errorf("patching failed: index: %d, func: %s, reason: %w", i, req.OldFuncEntrys[i].Name, err) 40 | } 41 | } 42 | req.Logger.Printf("monkey patching... finished") 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /picker.go: -------------------------------------------------------------------------------- 1 | package hotfix 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/go-hotfix/assembly" 10 | ) 11 | 12 | var closureExp = regexp.MustCompile(`func\d+(\.\d+)*`) 13 | 14 | // FuncPicker List of functions that need to be hotfix. 15 | type FuncPicker func(dwarfAssembly assembly.DwarfAssembly) ([]string, error) 16 | 17 | // Func to specify one or more functions, you must use the full qualified name of the function. 18 | // 19 | // example/data.TestAdd 20 | // example/data.(*DataType).TestHotfix 21 | // example/data.testPrivateFunc 22 | // example/data.(*DataType).test 23 | func Func(funcNames ...string) FuncPicker { 24 | return func(_ assembly.DwarfAssembly) ([]string, error) { 25 | for _, name := range funcNames { 26 | if closureExp.MatchString(name) { 27 | return nil, fmt.Errorf("closure unsupported: %s", name) 28 | } 29 | } 30 | return funcNames, nil 31 | } 32 | } 33 | 34 | // Classes To fix a specified class or classes (all member functions), the fully qualified name of the class must be used. 35 | // 36 | // example/data.DataType 37 | // *example/data.DataType 38 | func Classes(classNames ...string) FuncPicker { 39 | return func(dwarfAssembly assembly.DwarfAssembly) ([]string, error) { 40 | var methods []string 41 | for _, className := range classNames { 42 | // 查找类型 43 | classType, err := dwarfAssembly.FindType(className) 44 | if nil != err { 45 | return nil, fmt.Errorf("%w: class not found: %s", err, className) 46 | } 47 | 48 | // 检查类型必须是 struct/*struct 49 | if reflect.Struct != classType.Kind() && (reflect.Ptr != classType.Kind() || classType.Elem().Kind() != reflect.Struct) { 50 | return nil, fmt.Errorf("%s is not a struct or *struct (%s)", className, classType.String()) 51 | } 52 | 53 | isPtr := reflect.Ptr == classType.Kind() 54 | if isPtr { 55 | classType = classType.Elem() 56 | } 57 | 58 | var prefixName string 59 | if isPtr { 60 | // example.data.(*DataType).String 61 | prefixName = classType.PkgPath() + ".(*" + classType.Name() + ")." 62 | } else { 63 | // example.data.DataType.String 64 | prefixName = classType.PkgPath() + "." + classType.Name() + "." 65 | } 66 | 67 | dwarfAssembly.ForeachFunc(func(name string, pc uint64) bool { 68 | if strings.HasPrefix(name, prefixName) && !closureExp.MatchString(name) { 69 | methods = append(methods, name) 70 | } 71 | return true 72 | }) 73 | } 74 | 75 | return methods, nil 76 | } 77 | } 78 | 79 | // Package To fix all export, private, and member functions in one or more packages, the full package name must be used 80 | // 81 | // example/data 82 | func Package(pkgs ...string) FuncPicker { 83 | return func(dwarfAssembly assembly.DwarfAssembly) ([]string, error) { 84 | var methods []string 85 | for _, pkg := range pkgs { 86 | 87 | // example/data.testPrivateFunc 88 | // example/data.(*DataType).TestHotfix 89 | var prefixName = pkg 90 | 91 | dwarfAssembly.ForeachFunc(func(name string, pc uint64) bool { 92 | if strings.HasPrefix(name, prefixName) && !closureExp.MatchString(name) { 93 | methods = append(methods, name) 94 | } 95 | return true 96 | }) 97 | } 98 | return methods, nil 99 | } 100 | } 101 | 102 | // Any combine multiple FuncPicker 103 | func Any(funcPickers ...FuncPicker) FuncPicker { 104 | return func(dwarfAssembly assembly.DwarfAssembly) ([]string, error) { 105 | var methods []string 106 | for _, picker := range funcPickers { 107 | mm, err := picker(dwarfAssembly) 108 | if nil != err { 109 | return nil, err 110 | } 111 | methods = append(methods, mm...) 112 | } 113 | return methods, nil 114 | } 115 | } 116 | --------------------------------------------------------------------------------