Many people think that monkey patches exist only in dynamic languages, such as Ruby and Python. But that's not right. Because the computer is just a stupid machine, we can always make it do what we want it to do! Let's see how the functions in Go work, and how we can modify them at run time. This article will use a large number of Intel compilations, so I assume you can read the assembly code, or you are holding the [reference manual] while reading this article (https://software.intel.com/en-us/articles/ introduction-to-x64-assembly). * * If you are not interested in how the Monkey patch works, you just want to use it, you can be in [here] (Https://github.com/bouk/monkey) Find the corresponding library file * * Let's take a look at the code produced by the assembly code: "' Gopackage mainfunc A () int {return 1}func main () {print (A ())} ' > Example1.go by GitHub Hosting [view source files] (https://gist.github.com/bouk/17262666fae75dd24a25/raw/712ae5ef5b1becf4f782d96ca0be0d67ccdcf061/ EXAMPLE1.GO) The code above should be compiled with go build-gcflags=-l to avoid inline. In this article I assume that your computer architecture is 64-bit and that you are using a UNIX-based operating system such as Mac OSX or a Linux system. When the code is compiled, we use [Hopper] (http://hopperapp.com/) to see, we can see that the above code will produce the following assembly code:! [Image] (Https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-1.png) I will refer to the address of the various instructions displayed on the left side of the screen. Our code starts with the Main.main process, from the 0x2010 to the 0x2026 instruction build stack. You can get more relevant knowledge [here] (http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite), I will ignore this part in the following paragraphs of this articleCode. The 0x202a line is the MAIN.A function that invokes the 0x2000 line, which simply presses 0x1 into the stack and returns. 0x202f to 0x2037 This value is passed to Runtime.printint. Simple enough! Now let's look at how the values of functions are implemented in the Go language. # # The function values in the Go language work look at the following code: ' ' Gopackage mainimport ("FMT" "unsafe") func A () int {return 1}func main () {f: = a fmt. Printf ("0x%x\n", * (*uintptr) (unsafe.) Pointer (&f))} "> Funcval.go hosted by GitHub [view Source file] (https://gist.github.com/bouk/c921c3627ddbaae05356/raw/ 4C18DBAA7CFEB06B74007B65649D85F65384841A/FUNCVAL.GO) I assign a value to F in line 11th, which means that execution F () will call a. I then use the [unsafe] (http://golang.org/pkg/unsafe/) package in Go to read directly the values stored in F. If you have a C language development background, you can feel that f is a simple function pointer, and this code will output 0x2000 (the address of the MAIN.A that we see above). When I am running on my machine, I get 0x102c38, this address even with our code is not next to! When you decompile, this is the 11th line above:! [Image] (Https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-2.png) mentioned here a MAIN.A.F, when we check its address, we can see this:! [Image] (https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-3.png) Ah ha! The address of the MAIN.A.F is 0x102c38, and the saved value is 0x2000, and this is the address of MAIN.A. It looks like F doesn'tis a function pointer, but a pointer to a function pointer. Let's modify the code to eliminate the deviations. "' Gopackage mainimport (" FMT "" unsafe ") func A () int {return 1}func main () {f: = a fmt. Printf ("0x%x\n", * * (**UINTPTR) (unsafe. Pointer (&f))} "> Funcval2.go hosted by GitHub [view Source file] (https://gist.github.com/bouk/c470c4d80ae80d7b30af/raw/ D8BD9CD2B80CAD288993D5E8F67B115440C6C2A3/FUNCVAL2.GO) is now outputting the expected 0x2000. We can be in [here] (https://github.com/golang/go/blob/e9d9d0befc634f6e9f906b5ef7476fbd7ebd25e3/src/runtime/runtime2.go# l75-l78) Find a little clue as to why the code should write like this. In the Go language, function values can contain additional information, closures, and binding instance methods to achieve this. Let's see how calling a function value works. I'll modify the code above and call it directly after assigning f a value. "' Gopackage mainfunc A () int {return 1}func main () {f: = AF ()} ' > Callfuncval.go hosted by GitHub [view Source] (HTTPS://GIST.G Ithub.com/bouk/58bba533fb3b742ed964/raw/41821274ea8684f7b4c59e81dcc9df6c869c5bfd/callfuncval.go) When we decompile we can see: ! [Image] (https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-4.png) MAIN.A.F address is loaded into RDX , and then whatever the RDX points to will be loaded into RBX, and then RBX will be called. The address of the function is loaded into the RDX first, and then the called function can be used to load some additional possibleThe information. For bound instance methods and anonymous closure functions, the additional information is a pointer to an instance. If you want to learn more, I suggest you use the anti-compiler to delve into it yourself. Let's use the knowledge we just learned to implement monkey patches in Go. # # run time replace a function what we want to do is let the following code output 2: "' Gopackage mainfunc A () int {return 1}func B () int {return 2}func main () {replace (a), b) Print (A ())} "> Replace.go hosted by GitHub [view Source] (https://gist.github.com/bouk/713f3df2115e1b5e554d/raw/ 65335F4E7D9D0E11A5F72E78D617EC51249C577B/REPLACE.GO) Now how do we implement this substitution? Instead of executing its own function body, we need to modify the code of function A to jump to B. Essentially, we need to replace this by loading the function value of B into the RDX and jumping to the address the RDX points to. "Mov RDX, MAIN.B.F; C7 C2?? ?? ?? ?? JMP [RDX]; FF "> Replacement.asm hosted by GitHub [view Source] (https://gist.github.com/bouk/713f3df2115e1b5e554d/raw/ 65335F4E7D9D0E11A5F72E78D617EC51249C577B/REPLACE.GO) I list the corresponding machine code generated after the above code is compiled (using an online compiler, such as [This] (https://defuse.ca/ online-x86-assembler.htm), you can try to compile at will. Obviously, we need to write a function that produces such a machine code, and it should look like this: "' Gofunc assemblejump (f func () int) []byte {funcval: = * (*uintptr) (unsafe. Pointer (&F)) return []byte{0x48, 0xC7, 0xC2, byte (funcval >> 0), byte (Funcval >> 8), byte (Funcval >> ; (+), byte (Funcval >>),//MOV RDX, Funcval 0xFF, 0x22,//JMP [RDX]}} ' > Assemble_jump.go hosted by GitHub [view Source Pieces] (https://gist.github.com/bouk/4ed563abdcd06fc45fa0/raw/fa9c65c2d5828592e846e28136871ee0bd13e5a9/assemble_ JUMP.GO) Now we are ready to replace the function body of a with a jump from A to B! The following code attempts to copy the machine code directly into the function body. "' Gopackage mainimport (" syscall "" unsafe ") func A () int {return 1}func B () int {return 2}func rawmemoryaccess (b uintpt R) []byte {return (* (*[0xff]byte) (unsafe. Pointer (b))) [:]}func Assemblejump (f func () int) []byte {funcval: = * (*uintptr) (unsafe. Pointer (&F)) return []byte{0x48, 0xC7, 0xc2,byte (funcval >> 0), byte (Funcval >> 8), byte (Funcval >> (+), byte (Funcval >>),//MOV RDX, Funcval0xff, 0x22,//JMP [Rdx]}}func replace (orig, replacement func () int) {by TES: = assemblejump (Replacement) Functionlocation: = * * (**uintptr) (unsafe. Pointer (&orig)) window: = rawmemoryaccess (functionlocation) Copy (window, bytes)}func main () {replace (A, B) print (A ( )} "> Patch_attempt.goHosted by GitHub [view source files] (https://gist.github.com/bouk/4ed563abdcd06fc45fa0/raw/ FA9C65C2D5828592E846E28136871EE0BD13E5A9/ASSEMBLE_JUMP.GO) However, running the above code does not achieve our purpose, in fact, it produces a segment error. This is because the binary code that has already been loaded is not writable [by default] (Https://en.wikipedia.org/wiki/Segmentation_fault#Writing_to_read-only_memory). We can use the Mprotect system to cancel this protection, and the final version of the code is the same as we expected, the function A is replaced by B, and then ' 2 ' is printed out. "' Gopackage mainimport (" syscall "" unsafe ") func A () int {return 1}func B () int {return 2}func getpage (P uintptr) []byt e {return (* (*[0xffffff]byte) (unsafe. Pointer (P & ^uintptr (Syscall. GetPageSize ()-1))) [: Syscall. GetPageSize ()]}func rawmemoryaccess (b uintptr) []byte {return (* (*[0xff]byte) (unsafe. Pointer (b))) [:]}func Assemblejump (f func () int) []byte {funcval: = * (*uintptr) (unsafe. Pointer (&F)) return []byte{0x48, 0xC7, 0xc2,byte (funcval >> 0), byte (Funcval >> 8), byte (Funcval >> (+), byte (Funcval >>),//MOV RDX, Funcval0xff, 0x22,//JMP Rdx}}func replace (orig, replacement func () int) {byte S: = Assemblejump (Replacement) Functionlocation: = * (**uintptr) (unsafe. Pointer (&orig)) window: = rawmemoryaccess (functionlocation) Page: = GetPage (functionlocation) syscall. Mprotect (page, Syscall. Prot_read|syscall. Prot_write|syscall. prot_exec) Copy (window, bytes)}func main () {replace (A, B) print (A ())} "> Patch_success.go hosted by GitHub [view Source] (https:/ /GIST.GITHUB.COM/BOUK/55900E1D964099368AB0/RAW/976376012F9B073417A6CB68960458392B7F7952/PATCH_SUCCESS.GO) # # Packaged into a good library I wrap the above code as an [easy-to-use library] (Https://github.com/bouk/monkey). It supports 32 bits, cancels the patch, and patches The instance method, and I wrote several examples of use in the README. # # Summary a willing heart! It is possible for a program to modify itself at run time, which allows us to implement some cool things like monkey patches. I hope you have some gains from this blog, and I am very happy to write this article!
via:https://bou.ke/blog/monkey-patching-in-go/
Author: Bouke Translator: Moodwu proofreading: polaris1119
This article by GCTT original compilation, go language Chinese network honor launches
This article was originally translated by GCTT and the Go Language Chinese network. Also want to join the ranks of translators, for open source to do some of their own contribution? Welcome to join Gctt!
Translation work and translations are published only for the purpose of learning and communication, translation work in accordance with the provisions of the CC-BY-NC-SA agreement, if our work has violated your interests, please contact us promptly.
Welcome to the CC-BY-NC-SA agreement, please mark and keep the original/translation link and author/translator information in the text.
The article only represents the author's knowledge and views, if there are different points of view, please line up downstairs to spit groove
97 Reads