Source file src/runtime/hexdump.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package runtime
     6  
     7  import (
     8  	"internal/goarch"
     9  	"unsafe"
    10  )
    11  
    12  // hexdumpWords prints a word-oriented hex dump of [p, p+len).
    13  //
    14  // If mark != nil, it will be passed to hexdumper.mark.
    15  func hexdumpWords(p, len uintptr, mark func(uintptr, hexdumpMarker)) {
    16  	printlock()
    17  
    18  	// Provide a default annotation
    19  	symMark := func(u uintptr, hm hexdumpMarker) {
    20  		if mark != nil {
    21  			mark(u, hm)
    22  		}
    23  
    24  		// Can we symbolize this value?
    25  		val := *(*uintptr)(unsafe.Pointer(u))
    26  		fn := findfunc(val)
    27  		if fn.valid() {
    28  			hm.start()
    29  			print("<", funcname(fn), "+", hex(val-fn.entry()), ">\n")
    30  		}
    31  	}
    32  
    33  	h := hexdumper{addr: p, mark: symMark}
    34  	h.write(unsafe.Slice((*byte)(unsafe.Pointer(p)), len))
    35  	h.close()
    36  	printunlock()
    37  }
    38  
    39  // hexdumper is a Swiss-army knife hex dumper.
    40  //
    41  // To use, optionally set addr and wordBytes, then call write repeatedly,
    42  // followed by close.
    43  type hexdumper struct {
    44  	// addr is the address to print for the first byte of data.
    45  	addr uintptr
    46  
    47  	// addrBytes is the number of bytes of addr to print. If this is 0, it
    48  	// defaults to goarch.PtrSize.
    49  	addrBytes uint8
    50  
    51  	// wordBytes is the number of bytes in a word. If wordBytes is 1, this
    52  	// prints a byte-oriented dump. If it's > 1, this interprets the data as a
    53  	// sequence of words of the given size. If it's 0, it's treated as
    54  	// goarch.PtrSize.
    55  	wordBytes uint8
    56  
    57  	// mark is an optional function that can annotate values in the hex dump.
    58  	//
    59  	// If non-nil, it is called with the address of every complete, aligned word
    60  	// in the hex dump.
    61  	//
    62  	// If it decides to print an annotation, it must first call m.start(), then
    63  	// print the annotation, followed by a new line.
    64  	mark func(addr uintptr, m hexdumpMarker)
    65  
    66  	// Below here is state
    67  
    68  	ready int8 // 0=need to init state; 1=need to print header; 2=ready
    69  
    70  	// dataBuf accumulates a line at a time of data, in case it's split across
    71  	// buffers.
    72  	dataBuf  [16]byte
    73  	dataPos  uint8
    74  	dataSkip uint8 // Skip first n bytes of buf on first line
    75  
    76  	// toPos maps from byte offset in data to a visual offset in the printed line.
    77  	toPos [16]byte
    78  }
    79  
    80  type hexdumpMarker struct {
    81  	chars int
    82  }
    83  
    84  func (h *hexdumper) write(data []byte) {
    85  	if h.ready == 0 {
    86  		h.init()
    87  	}
    88  
    89  	// Handle leading data
    90  	if h.dataPos > 0 {
    91  		n := copy(h.dataBuf[h.dataPos:], data)
    92  		h.dataPos += uint8(n)
    93  		data = data[n:]
    94  		if h.dataPos < uint8(len(h.dataBuf)) {
    95  			return
    96  		}
    97  		h.flushLine(h.dataBuf[:])
    98  		h.dataPos = 0
    99  	}
   100  
   101  	// Handle full lines in data
   102  	for len(data) >= len(h.dataBuf) {
   103  		h.flushLine(data[:len(h.dataBuf)])
   104  		data = data[len(h.dataBuf):]
   105  	}
   106  
   107  	// Handle trailing data
   108  	h.dataPos = uint8(copy(h.dataBuf[:], data))
   109  }
   110  
   111  func (h *hexdumper) close() {
   112  	if h.dataPos > 0 {
   113  		h.flushLine(h.dataBuf[:h.dataPos])
   114  	}
   115  }
   116  
   117  func (h *hexdumper) init() {
   118  	const bytesPerLine = len(h.dataBuf)
   119  
   120  	if h.addrBytes == 0 {
   121  		h.addrBytes = goarch.PtrSize
   122  	} else if h.addrBytes < 0 || h.addrBytes > goarch.PtrSize {
   123  		throw("invalid addrBytes")
   124  	}
   125  
   126  	if h.wordBytes == 0 {
   127  		h.wordBytes = goarch.PtrSize
   128  	}
   129  	wb := int(h.wordBytes)
   130  	if wb < 0 || wb >= bytesPerLine || wb&(wb-1) != 0 {
   131  		throw("invalid wordBytes")
   132  	}
   133  
   134  	// Construct position mapping.
   135  	for i := range h.toPos {
   136  		// First, calculate the "field" within the line, applying byte swizzling.
   137  		field := 0
   138  		if goarch.BigEndian {
   139  			field = i
   140  		} else {
   141  			field = i ^ int(wb-1)
   142  		}
   143  		// Translate this field into a visual offset.
   144  		// "00112233 44556677  8899AABB CCDDEEFF"
   145  		h.toPos[i] = byte(field*2 + field/4 + field/8)
   146  	}
   147  
   148  	// The first line may need to skip some fields to get to alignment.
   149  	// Round down the starting address.
   150  	nAddr := h.addr &^ uintptr(bytesPerLine-1)
   151  	// Skip bytes to get to alignment.
   152  	h.dataPos = uint8(h.addr - nAddr)
   153  	h.dataSkip = uint8(h.addr - nAddr)
   154  	h.addr = nAddr
   155  
   156  	// We're ready to print the header.
   157  	h.ready = 1
   158  }
   159  
   160  func (h *hexdumper) flushLine(data []byte) {
   161  	const bytesPerLine = len(h.dataBuf)
   162  
   163  	const maxAddrChars = 2 * goarch.PtrSize
   164  	const addrSep = ": "
   165  	dataStart := int(2*h.addrBytes) + len(addrSep)
   166  	// dataChars uses the same formula to toPos above. We calculate it with the
   167  	// "last field", then add the size of the last field.
   168  	const dataChars = (bytesPerLine-1)*2 + (bytesPerLine-1)/4 + (bytesPerLine-1)/8 + 2
   169  	const asciiSep = "  "
   170  	asciiStart := dataStart + dataChars + len(asciiSep)
   171  	const asciiChars = bytesPerLine
   172  	nlPos := asciiStart + asciiChars
   173  
   174  	var lineBuf [maxAddrChars + len(addrSep) + dataChars + len(asciiSep) + asciiChars + 1]byte
   175  	clear := func() {
   176  		for i := range lineBuf {
   177  			lineBuf[i] = ' '
   178  		}
   179  	}
   180  	clear()
   181  
   182  	if h.ready == 1 {
   183  		// Print column offsets header.
   184  		for offset, pos := range h.toPos {
   185  			h.fmtHex(lineBuf[dataStart+int(pos+1):][:1], uint64(offset))
   186  		}
   187  		// Print ASCII offsets.
   188  		for offset := range asciiChars {
   189  			h.fmtHex(lineBuf[asciiStart+offset:][:1], uint64(offset))
   190  		}
   191  		lineBuf[nlPos] = '\n'
   192  		gwrite(lineBuf[:nlPos+1])
   193  		clear()
   194  		h.ready = 2
   195  	}
   196  
   197  	// Format address.
   198  	h.fmtHex(lineBuf[:2*h.addrBytes], uint64(h.addr))
   199  	copy(lineBuf[2*h.addrBytes:], addrSep)
   200  	// Format data in hex and ASCII.
   201  	for offset, b := range data {
   202  		if offset < int(h.dataSkip) {
   203  			continue
   204  		}
   205  
   206  		pos := h.toPos[offset]
   207  		h.fmtHex(lineBuf[dataStart+int(pos):][:2], uint64(b))
   208  
   209  		copy(lineBuf[dataStart+dataChars:], asciiSep)
   210  		ascii := uint8('.')
   211  		if b >= ' ' && b <= '~' {
   212  			ascii = b
   213  		}
   214  		lineBuf[asciiStart+offset] = ascii
   215  	}
   216  	// Trim buffer.
   217  	end := asciiStart + len(data)
   218  	lineBuf[end] = '\n'
   219  	buf := lineBuf[:end+1]
   220  
   221  	// Print.
   222  	gwrite(buf)
   223  
   224  	// Print marks.
   225  	if h.mark != nil {
   226  		clear()
   227  		for offset := 0; offset+int(h.wordBytes) <= len(data); offset += int(h.wordBytes) {
   228  			if offset < int(h.dataSkip) {
   229  				continue
   230  			}
   231  			addr := h.addr + uintptr(offset)
   232  			// Find the position of the left edge of this word
   233  			caret := dataStart + int(min(h.toPos[offset], h.toPos[offset+int(h.wordBytes)-1]))
   234  			h.mark(addr, hexdumpMarker{caret})
   235  		}
   236  	}
   237  
   238  	h.addr += uintptr(bytesPerLine)
   239  	h.dataPos = 0
   240  	h.dataSkip = 0
   241  }
   242  
   243  // fmtHex formats v in base 16 into buf. It fills all of buf. If buf is too
   244  // small to represent v, it the output will start with '*'.
   245  func (h *hexdumper) fmtHex(buf []byte, v uint64) {
   246  	const dig = "0123456789abcdef"
   247  	i := len(buf) - 1
   248  	for ; i >= 0; i-- {
   249  		buf[i] = dig[v%16]
   250  		v /= 16
   251  	}
   252  	if v != 0 {
   253  		// Indicate that we couldn't fit the whole number.
   254  		buf[0] = '*'
   255  	}
   256  }
   257  
   258  func (m hexdumpMarker) start() {
   259  	var spaces [64]byte
   260  	for i := range spaces {
   261  		spaces[i] = ' '
   262  	}
   263  	for m.chars > len(spaces) {
   264  		gwrite(spaces[:])
   265  		m.chars -= len(spaces)
   266  	}
   267  	gwrite(spaces[:m.chars])
   268  	print("^ ")
   269  }
   270  

View as plain text