© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
N. TolaramSoftware Development with Gohttps://doi.org/10.1007/978-1-4842-8731-6_2

2. System Calls Using Go

Nanik Tolaram1  
(1)
Sydney, NSW, Australia
 

In this chapter, you will explore writing applications that perform system-level operations using system calls. The operating system provides a lot of ways for applications to extract information and perform operations. You will look at the different ways to extract system-level information and use both the Go standard library and system files.

In this chapter, you will learn the following:
  • How to use syscall packages

  • How to understand and read ELF format files

  • How to use the /sys filesystem

  • How to write a simple application to read disk statistics

Source Code

The source code for this chapter is available from the https://github.com/Apress/Software-Development-Go repository.

Syscall Package

The syscall package is the standard library provided by Go that provides function calls that interface with the log-level operating system. The following are some of the functionalities provided by the package:
  • Change directory

  • Duplicate file descriptor

  • Get current working directory

  • …and many more

syscall Application

Let’s take the existing application from Chapter 1 and convert it to use the syscall package. The app can be seen inside the chapter2/syscalls directory. Open terminal and run the sample as follows:
go run main.go
You will see the following output:
2022/07/17 19:20:42 Getpid :  23815
2022/07/17 19:20:42 Getpgrp :  23712
2022/07/17 19:20:42 Getpgrp :  23712
2022/07/17 19:20:42 Gettid :  23815
2022/07/17 19:20:42 /home/nanik/go/chapter2/syscal
The sample code uses system calls to get information about itself such as the process id assigned by the operating system for itself, the parent id, and others. The following shows how it uses the syscall package:
package main
import (
  "log"
  s "syscall"
)
func main() {
  ...
  log.Println("Getpid : ", s.Getpid())
  ...
  _, err := s.Getcwd(c)
  ...
}

The code is the same except for replacing the golang.org/x/sys/unix package with the syscall package, while the function call remains the same.

Figure 2-1 shows the comparison between the sys/unix and syscall packages. As you can see, there are functions providing the same functionality available in both packages.

A screenshot of the user interface of Go Unix and syscall software represents options such as documentation, overview, index, examples, constants, variables, functions, types, source files, and directories.

Figure 2-1

sys/unix vs. syscall

Checking Disk Space

You are going to take a look at an example application that can be found inside the chapter2/diskspace directory. The application uses the syscall package to obtain hard disk information such as free space, total space, and such.

Open terminal and run the sample as follows:
go run main.go
You will see the following output:
Total Disk Space : 460.1 GB
Total Disk Used  : 322.4 GB
Total Disk Free  : 137.7 GB
The output shows in gigabytes the total size of the drive, total amount of disk used, and total amount of disk free. The following code snippet shows how the disk information is obtained using the syscall package:
func main() {
  var statfs = syscall.Statfs_t{}
  var total uint64
  var used uint64
  var free uint64
  err := syscall.Statfs("/", &statfs)
  if err != nil {
     fmt.Printf("[ERROR]: %s ", err)
  } else {
     total = statfs.Blocks * uint64(statfs.Bsize)
     free = statfs.Bfree * uint64(statfs.Bsize)
     used = total - free
  }
  ...
}
As seen in the above code snippet, the application uses the syscall.Statfs function call to get information about the path. In this case, it’s the root directory. The result is populated into the statfs variable, which is of type Statfs_t. The Statfs_t struct declaration looks like the following:
type Statfs_t struct {
  Type    int64
  Bsize   int64
  Blocks  uint64
  Bfree   uint64
  Bavail  uint64
  Files   uint64
  Ffree   uint64
  Fsid    Fsid
  Namelen int64
  Frsize  int64
  Flags   int64
  Spare   [4]int64
}

Webserver with syscall

Let’s take a look at another example using the syscall package, which can be found inside the chapter2/webserversyscall directory. The sample code is a web server that uses the syscall package to create a socket connection.

Open terminal and run the sample as follows:
go run main.go
You will see the following output:
2022/07/17 19:27:49 Listening on  127.0.0.1 : 8888

The web server is now ready to accept connection on port 8888. Open your browser and type in http://localhost:8888. You will get a response in your browser: Server with syscall

The following code snippet shows the function that takes care of starting up the server that listens on port 8888:
func startServer(host string, port int) (int, error) {
  fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
  if err != nil {
     log.Fatal("error (listen) : ", err)
  }
  sa := &syscall.SockaddrInet4{Port: port}
  addrs, err := net.LookupHost(host)
  ...
  for _, addr := range addrs {
  ...
  }
  ...
  return fd, nil
}
The code performs the following process:
  • Creates a socket

  • Binds a socket to port 8888

  • Listens for an incoming request

The code use syscall.Socket to create a socket. Once it is able to create a socket, it will bind it to the specified port 8888 by calling syscall.Bind, as shown in the following code snippet:
for _, addr := range addrs {
  ...
  if err = syscall.Bind(fd, srv); err != nil {
     log.Fatal("error (bind) : ", err)
  }
}
On successful completion of the binding process, the code starts listening for incoming requests, as shown here:
if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
  log.Fatal("error (listening) : ", err)
} else {
  log.Println("Listening on ", host, ":", port)
}

The syscall.Listen is called, passing syscall.SOMAXCONN as the parameter. This instructs the operating system that the code wants to have the maximum queue allocated to take care of pending connections when they happen. Now the server is ready to accept connections.

The next part of the code accepts and processes incoming requests, which can be seen in the following code snippet:
for {
  cSock, cAddr, err := syscall.Accept(fd)
  if err != nil {
    ...
  }
  go func(clientSocket int, clientAddress syscall.Sockaddr) {
     err := syscall.Sendmsg(clientSocket, []byte(message), []byte{}, clientAddress, 0)
     ...
     syscall.Close(clientSocket)
  }(cSock, cAddr)
}

The code uses syscall.Accept to start accepting incoming requests, as can be seen in the for{} loop. On every accepted request, the code processes the request by processing it in a separate go routine. This allows the server to be able to process incoming requests without being blocked.

ELF Package

The standard library provides different packages that can be used to interact with different parts of the operating system. In the previous sections, you looked at interacting on a system level by using the different standard library packages. In this section, you will look at the debug/elf package.

This package provides interfaces for applications to interact with ELF files. ELF stands for the Executable Linkable Format, which means that an ELF file can be an executable or object file that is used for linking processes to create an executable file. I will not go into detail on ELF; more information can be found at https://linux.die.net/man/5/elf.

High-Level ELF Format

ELF is a common standard file format for executable files, object code, shared libraries, and core dumps; it is cross platform. Figure 2-2 shows at high level the structure of an ELF file.

A block diagram of the E L F file represents the header, program header table, dot text file, dot rodata, dot data, and section header table.

Figure 2-2

ELF file structure

Figure 2-3 shows output of the header section of a sample application compiled on my local machine.

A screenshot of E L F header code represents class, data, version, ABI versions, type, flags, size of headers, size of section headers, number of section headers, and string table index.

Figure 2-3

ELF file header section

Dump Example

In this section, you will take a look at an open source project named GoPlay, which is hosted at https://github.com/n4ss/GoPlay. It can also be found inside the chapter2/GoPlay directory. This is a simple app that dumps the contents of a Go ELF executable file. You will look at how the application uses the Go library to read the ELF file

Compile the GoPlay application to create an executable using the following command:
go build main.go
Now compile GoPlay and run it as follows:
./goplay -action=dump -filename=./goplay
You are instructing GoPlay to dump the contents of the goplay executable, which will give you output something like the following:
Tracing program : "[path]goplay".
Action : "dump".
DynStrings:
Symbols:
      go.go
      runtime.text
      cmpbody
      countbody
      memeqbody
      indexbody
      indexbytebody
      gogo
      callRet
      gosave_systemstack_switch
      setg_gcc
      aeshashbody
      debugCall32
      debugCall64
      ....
      runtime.(*cpuProfile).addNonGo
      ....
       _cgo_init
       runtime.mainPC
       go.itab.syscall.Errno,error
       runtime.defaultGOROOT.str
       runtime.buildVersion.str
       type.*
       runtime.textsectionmap
   ....
Let’s start analyzing how the code works and what system calls it is using to get what information out from the executable file.
func main() {
   ....
       file, err := os.Stat(*filename)
   ....
       f, err := os.Open(*filename)
   ....
       switch *action {
   ....
       case "dump": os.Exit(dump_elf(*filename))
       }
   } else {
       goto Usage
   }
   ....
}
On startup, the application uses the os.Stat system call to check whether the executable file specified as the parameter exists and opens it using os.Open if it does exist. Once open, it will use the function dump_elf(..) to dump the file contents. The following is a snippet of the function:
func dump_elf(filename string) int {
   file, err := elf.Open(filename)
   if err != nil {
       fmt.Printf("Couldn't open file : "%s" as an ELF. ")
       return 2
   }
   dump_dynstr(file)
   dump_symbols(file)
   return 0
}

The function uses another system call named elf.Open, which is available inside the debug/elf package. This is similar to the os.Open function but with the additional functionality that the opened file is prepared to be read as an ELF file. On returning from calling elf.Open, the returned file variable will be populated with information about the internals of the ELF file.

Once the file is open, it calls dump_symbols to dump the file contents. The dump_symbols function dumps all symbols information from the file, which is made available by calling the file.Symbols() function. The application just prints the Name field.
func dump_symbols(file *elf.File) {
   fmt.Printf("Symbols: ")
   symbols, _ := file.Symbols()
   for _, e := range symbols {
       if !strings.EqualFold(e.Name, "") {
           fmt.Printf(" %s ", e.Name)
       }
   }
}
The following is the struct definition of the Symbol struct. As you can see, it contains useful information.
type Symbol struct {
  Name        string
  Info, Other byte
  Section     SectionIndex
  Value, Size uint64
  // Version and Library are present only for the dynamic symbol
  // table.
  Version string
  Library string
}
The other function called to dump ELF information is dump_dynstr:
func dump_dynstr(file *elf.File) {
  fmt.Printf("DynStrings: ")
  dynstrs, _ := file.DynString(elf.DT_NEEDED)
  ...
  dynstrs, _ = file.DynString(elf.DT_SONAME)
  ...
  dynstrs, _ = file.DynString(elf.DT_RPATH)
  ...
  dynstrs, _ = file.DynString(elf.DT_RUNPATH)
  ...
}
This function is used to obtain certain parts of the ELF file, which are passed as parameters when calling the file.DynString function. For example, when calling
dynstrs, _ = file.DynString(elf.DT_SONAME)

the code will get information about the shared library name of the file.

/sys Filesystem

In this section, you will look at a different way of reading system-level information. You will not use a function to read system information; rather, you will use system directories that are made available by the operating system for user applications.

The directory that you want to read is the /sys directory, which is a virtual filesystem containing device drivers, device information, and other kernel features. Figure 2-4 shows what the /sys directory contains on a Linux machine.

A screenshot of the code of a system directory representing the month name with root values. Different files listed are block, bus, class, dev, devices, firmware, fs, hypervisor, kernel, and module.

Figure 2-4

Inside the /sys directory

Reading AppArmor

Some of the information that is provided by Linux inside the /sys directory is related to AppArmor (short for Application Armor). What is AppArmor? It is a kernel security module that gives system administrators the ability to restrict application capabilities with a profile. This gives system administrators the power to select which resources a particular application can have access to. For example, a system administrator can define Application A to have network access or raw socket access, while Application B does not have access to network capabilities.

Let’s look at an example application to read AppArmor information from the /sys filesystem, specifically whether AppArmor is enabled and whether it is enforced. The following is the sample code that can be found inside the chapter2/apparmor directory:
import (
  "fmt"
   ...
)
const (
  appArmorEnabledPath = "/sys/module/apparmor/parameters/enabled"
  appArmorModePath    = "/sys/module/apparmor/parameters/mode"
)
func appArmorMode() (mode string) {
  content, err := ioutil.ReadFile(appArmorModePath)
  ...
  return strings.TrimSpace(string(content))
}
func appArmorEnabled() (support bool) {
  content, err := ioutil.ReadFile(appArmorEnabledPath)
  ...
  return strings.TrimSpace(string(content)) == "Y"
}
func main() {
  fmt.Println("AppArmor mode : ", appArmorMode())
  fmt.Println("AppArmor is enabled : ", appArmorEnabled())
}
Since the code is accessing a system filesystem, you must run it using root. Compile the code and run it as follows:
sudo ./apparmor

The code reads the information from the directory using the standard library ioUtil.ReadFile, which is just like reading a file, so it’s simpler than using the function calls that you looked at in the previous sections.

Summary

In this chapter, you looked at using system calls to interface with the operating system. You looked at using the syscall standard library that provides a lot of function calls to interface with the operating system and wrote a sample application to print out disk space information. You looked at how the debug/elf standard library is used to read Go ELF file information. Lastly, you looked at the /sys filesystem to extract information that you want to read to understand whether the operating system supports AppArmor.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.235.63