Reversing-golang Reversing Go - Part 4
Post
Cancel

Reversing Go - Part 4

Understanding Channels

Channels are represented as pointers to hchan

1
2
3
4
5
6
7
8
9
10
11
12
13
type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters
    lock mutex
}

Let’s use the sample code from Concurrent Prime Sieve

Creating channels

makechan is used to create a channel

1
2
3
4
5
6
7
8
9
10
11
.text:004A58E8    lea     rax, chan_int
.text:004A58EF    mov     [rsp+80h+var_80], rax
.text:004A58F3    mov     [rsp+80h+var_78], 0
.text:004A58FC    nop     dword ptr [rax+00h]
.text:004A5900    call    runtime_makechan
.text:004A5905    mov     rax, [rsp+80h+var_70]
.text:004A590A    mov     [rsp+80h+var_20], rax
.text:004A590F    mov     dword ptr [rsp+80h+var_80], 8
.text:004A5916    lea     rcx, off_4D36C8   ; main_Generate
.text:004A591D    mov     [rsp+80h+var_78], rcx
.text:004A5922    call    runtime_newproc

A channel of type int is created and the pointer to the hchan instance is stored in var_20. And then, we have a call to newproc which spawns a goroutine - main.Generate using a stack size of 8 (means there is one argument). From the source code, we have

The stack layout of this call is unusual: it assumes that the arguments to pass to fn are on the stack sequentially immediately after &fn. Hence, they are logically part of newproc’s argument frame, even though they don’t appear in its signature (and can’t because their types differ between call sites).

It means that our new goroutine - main.Generate has one argument in var_70 which is the channel instance.

Sending Data

chansend1 is used to send data to channel. main.Generate is a small function

1
2
3
4
5
6
7
8
9
10
11
12
.text:004A57C4    mov     eax, 2
.text:004A57C9 loc_4A57C9:
.text:004A57C9    mov     [rsp+28h+var_18], rax
.text:004A57CE    mov     [rsp+28h+var_10], rax
.text:004A57D3    mov     rcx, [rsp+28h+arg_0]
.text:004A57D8    mov     [rsp+28h+var_28], rcx ; *hchan
.text:004A57DC    lea     rdx, [rsp+28h+var_10]
.text:004A57E1    mov     [rsp+28h+var_20], rdx
.text:004A57E6    call    runtime_chansend1
.text:004A57EB    mov     rax, [rsp+28h+var_18]
.text:004A57F0    inc     rax
.text:004A57F3    jmp     short loc_4A57C9

It runs an infinite loop sending i (i >= 2) to the channel (var_70 at main).

Receiving Data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:004A5927    xor     eax, eax
.text:004A5929    mov     rcx, [rsp+80h+var_20]
.text:004A592E    jmp     loc_4A5A32
.text:004A5933 loc_4A5933:
.text:004A5933    mov     [rsp+80h+var_38], rax
.text:004A5938    mov     [rsp+80h+var_20], rcx
.text:004A593D    mov     [rsp+80h+var_30], 0
.text:004A5946    mov     [rsp+80h+var_80], rcx
.text:004A594A    lea     rax, [rsp+80h+var_30]
.text:004A594F    mov     [rsp+80h+var_78], rax
.text:004A5954    call    runtime_chanrecv1
.text:004A5959    mov     rax, [rsp+80h+var_30]
; ... snip ...
.text:004A5A25    mov     rax, [rsp+80h+var_38]
.text:004A5A2A    inc     rax
.text:004A5A2D    mov     rcx, [rsp+80h+var_28]
.text:004A5A32 loc_4A5A32:
.text:004A5A32    cmp     rax, 0Ah
.text:004A5A36    jl      loc_4A5933

Back to main, it runs a loop 10 times, calling chanrecv1. The recv operator (“<-“) has two forms -

1
2
a, ok := <-ch   ; calls `chanrecv2`
a := <-ch       ; calls `chanrecv1`

Here, we are fetching one item from the channel (var_20) into var_30. I skipped the fmt.Println part.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:004A59CF    lea     rax, chan_int
.text:004A59D6    mov     [rsp+80h+var_80], rax
.text:004A59DA    mov     [rsp+80h+var_78], 0
.text:004A59E3    call    runtime_makechan
.text:004A59E8    mov     rax, [rsp+80h+var_70]
.text:004A59ED    mov     [rsp+80h+var_28], rax
.text:004A59F2    mov     dword ptr [rsp+80h+var_80], 18h
.text:004A59F9    lea     rcx, off_4D36C0   ; main_Filter
.text:004A5A00    mov     [rsp+80h+var_78], rcx
.text:004A5A05    mov     rdx, [rsp+80h+var_20]
.text:004A5A0A    mov     [rsp+80h+var_70], rdx
.text:004A5A0F    mov     [rsp+80h+var_68], rax
.text:004A5A14    mov     rdx, [rsp+80h+var_40]
.text:004A5A19    mov     [rsp+80h+var_60], rdx
.text:004A5A1E    xchg    ax, ax
.text:004A5A20    call    runtime_newproc

In every iteration, a new channel is created. And a goroutine - main.Filter is spawned. The first argument to newproc is 0x18 means there are 0x18/8 = 3 stack words as arguments to the function main.Filter. The arguments for main.Filter start at var_70 - the old channel (var_20), the new channel (created just now), and the value received from var_20 at 0x004A5954. The new channel is assigned to var_20 and it continues 10 times

main.Filter runs an infinite loop

1
2
3
4
5
6
.text:004A5828    mov     [rsp+20h+var_10], 0
.text:004A5831    mov     rax, [rsp+20h+arg_0]
.text:004A5836    mov     [rsp+20h+var_20], rax
.text:004A583A    lea     rcx, [rsp+20h+var_10]
.text:004A583F    mov     [rsp+20h+var_18], rcx
.text:004A5844    call    runtime_chanrecv1

It fetches an integer from the channel (passed as the first argument) and divides it by the 3rd argument (signed division)

1
2
3
4
5
6
7
8
.text:004A5872    test    rdx, rdx
.text:004A5875    jz      short loc_4A5828
.text:004A5877    mov     [rsp+20h+var_10], rbx
.text:004A587C    mov     rax, [rsp+20h+arg_8]
.text:004A5881    mov     [rsp+20h+var_20], rax
.text:004A5885    lea     rcx, [rsp+20h+var_10]
.text:004A588A    mov     [rsp+20h+var_18], rcx
.text:004A588F    call    runtime_chansend1

if the remainder of the division is non-zero, the integer fetched is sent to the channel (passed as the second argument)

Understanding Select

The empty select statement select{} is translated into runtime.block. A select with one case, is equivalent to chansend1 or chanrecv1 if we are sending/receiving data.

For two or more cases, runtime.selectgo is used

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func fibonacci(c chan int, ticker <-chan time.Time, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case tick := <-ticker:
			fmt.Printf("Tick! - %d\n", tick.Nanosecond())
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	ticker := time.NewTicker(1 * time.Nanosecond)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, ticker.C, quit)
}

Without much ado, let’s look at fibonacci function.

1
2
3
4
5
6
.text:004B3636    lea     r8, [rsp+130h+var_80]
.text:004B363E    mov     [rsp+130h+var_130], r8
.text:004B3642    lea     r8, [rsp+130h+var_C4]
.text:004B3647    mov     [rsp+130h+var_128], r8
.text:004B364C    mov     [rsp+130h+var_120], 3     ; 3 cases in select
.text:004B3655    call    runtime_selectgo

selectgo has the following signature

1
2
3
4
5
6
7
8
9
type scase struct {
	c           *hchan         // chan
	elem        unsafe.Pointer // data element
	kind        uint16  // 1 (receive), 2 (send), 3 (send+receive)
	pc          uintptr // race pc (for race detector / msan)
	releasetime int64
}

func selectgo(cases *scase, orders *uint16, ncases int) (int, bool)

Every case in select (with atleast two cases), is represented as an scase

1
2
3
4
5
.text:004B35CB    mov     word ptr [rsp+130h+var_80.kind], 2
.text:004B35D5    mov     rdx, [rsp+130h+arg_0]
.text:004B35DD    mov     [rsp+130h+var_80.chan], rdx
.text:004B35E5    lea     rbx, [rsp+130h+var_D0]
.text:004B35EA    mov     [rsp+130h+var_80.elem], rbx

var_80 contains the first scase:

  1. it uses arg_0 as the channel
  2. kind=2 i.e., it sends data present in var_D0 to arg_0
1
2
3
4
5
.text:004B35F2    mov     word ptr [rsp+130h+var_58.kind], 1
.text:004B35FC    mov     rbx, [rsp+130h+arg_8]
.text:004B3604    mov     [rsp+130h+var_58.chan], rbx
.text:004B360C    lea     rsi, [rsp+130h+var_98]
.text:004B3614    mov     [rsp+130h+var_58.elem], rsi

var_58 contains the second scase:

  1. it uses arg_8 as the channel
  2. kind=1 i.e., it receives data from channel arg_8 and stores in var_98
1
2
3
.text:004B361C    mov     word ptr [rsp+130h+var_30.kind], 1
.text:004B3626    mov     rsi, [rsp+130h+arg_10]
.text:004B362E    mov     [rsp+130h+var_30.chan], rsi

var_30 contains the third scase:

  1. it uses arg_10 as the channel
  2. kind=1 i.e., it receives and discards data, since elem is null

selectgo returns an integer denoting the index of the scase that can be executed without blocking and a boolean denoting if the chosen scase was a receive operation, it returns whether the value was received

Selected Case 0

This case is selected if the send operation using var_D0 to arg_0 can succeed

1
2
3
4
5
6
7
8
.text:004B3665    mov     rax, [rsp+130h+var_E0]
.text:004B366A    mov     rcx, [rsp+130h+var_D8]
.text:004B366F    add     rcx, rax
.text:004B3672    jmp     loc_4B3568
.text:004B3568 loc_4B3568:
.text:004B3568    mov     [rsp+130h+var_E0], rcx
.text:004B356D    mov     [rsp+130h+var_D8], rax
.text:004B3572    mov     [rsp+130h+var_D0], rax

var_D0 contains the same value as var_D8. This code computes the next fibonacci number and sends it to channel arg_0 using var_D0

Selected Case 1

This case is selected if the receive operation from channel arg_8 succeeded.

1
2
3
4
5
6
.text:004B368A    mov     rax, [rsp+130h+var_98]
.text:004B3692    and     rax, 3FFFFFFFh
.text:004B3698    movsxd  rax, eax
.text:004B369B    mov     [rsp+130h+var_130], rax
.text:004B369F    nop
.text:004B36A0    call    runtime_convT64

It uses fmt.Printf to print the value received from the channel at arg_8

Selected Case 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:004B372B    cmp     rax, 2
.text:004B372F    jnz     short loc_4B379C
.text:004B3731    xorps   xmm0, xmm0
.text:004B3734    movups  [rsp+130h+var_B8], xmm0
.text:004B3739    lea     rax, string_autogen_7VPPCB
.text:004B3740    mov     qword ptr [rsp+130h+var_B8], rax
.text:004B3745    lea     rax, off_4FA970 ; "quit"
.text:004B374C    mov     qword ptr [rsp+130h+var_B8+8], rax
.text:004B3754    mov     rax, cs:os_Stdout
.text:004B375B    lea     rcx, go_itab__os_File_io_Writer
.text:004B3762    mov     [rsp+130h+var_130], rcx
.text:004B3766    mov     [rsp+130h+var_128], rax
.text:004B376B    lea     rax, [rsp+130h+var_B8]
.text:004B3770    mov     [rsp+130h+var_120], rax
.text:004B3775    mov     [rsp+130h+var_118], 1
.text:004B377E    mov     [rsp+130h+var_110], 1
.text:004B3787    call    fmt_Fprintln

It prints “quit” to using fmt.Println and returns. Now we can construct the select statement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func fibonacci(a chan int, b chan int64, c chan int) {
    d, e := 0, 1
    for {
        select {
            case a <- d:
                d, e = e, d+e
            case t := <- b:
                fmt.Printf("Tick! - %d\n", t & 0x3FFFFFFF)
            case <-c:
                fmt.Println("quit")
                return
        }
    }
}

Can we do better? Yes we can!

The channel a is never read, and the channels b and c are never written. Clearing up, we get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func fibonacci(a chan<- int, b <-chan int64, c <-chan int) {
    d, e := 0, 1
    for {
        select {
            case a <- d:
                d, e = e, d+e
            case t := <-b:
                fmt.Printf("Tick! - %d\n", t & 0x3FFFFFFF)
            case <-c:
                fmt.Println("quit")
                return
        }
    }
}

This is the raw representation from the assembly. To refine it more, we need to understand main function. Stay tuned for the next part!

This post is licensed under CC BY 4.0 by the author.