Systems Programming
Concepts of Programming Languages
Sebastian Macke
Rosenheim Technical University
Sebastian Macke
Rosenheim Technical University
Definition
"Systems programming involves designing and writing computer programs that allow the computer hardware to interface with the programmer and the user, leading to the effective execution of application software on the computer system.
Typical programs include the operating system and firmware, programming tools such as compilers, assemblers, I/O routines, interpreters, scheduler, loaders and linkers as well as the runtime libraries of the computer programming languages."
3Also most the compiling programming languages incorporate assembler
5#include<stdlib.h> #include<stdio.h> void main() { srandom(1); printf("random int is %li\n", random()); }
run with
gcc random.c -o random ./random random number is ...
package main // #include<stdlib.h> // #include<stdio.h> // void Cmain() { // srandom(1); // printf("random int is %li\n", random()); // } import "C" func main() { C.Cmain() }
Import the pseudo package C in your Go source and include the corresponding C header files (comment) to call a C function.
package rand // #include <stdlib.h> import "C" func Random() int { var r C.long = C.random() // calls "int rand(void)" from the C std library return int(r) } func Seed(i int) { C.srandom(C.uint(i)) }
//go:build linux // +build linux package main // #include <stdlib.h> import "C" import "fmt" func Seed(i int) { C.srandom(C.uint(i)) } func Random() int { var r C.long = C.random() // calls "int rand(void)" from the C std library return int(r) } func main() { Seed(1) fmt.Printf("random int from C is %d\n", Random()) }
Build flags for the cgo command can be set in the Go source file as comments:
// #cgo CFLAGS: -DPNG_DEBUG=1 // #cgo amd64 386 CFLAGS: -DX86=1 // #cgo LDFLAGS: -lpng // #include <png.h> import "C"
When calling functions from a library outside libc, the Go programm must be linked with that library (here: png.lib/png.so).
This can be done by adding cgo build flags to the comment section.
11int
→ C.int
, unsigned
short
→ C.ushort
, etc.void*
is unsafe.Pointer
struct_
prefix, e.g. struct
foo
→ C.struct_foo
, same goes for unions and enums
The C package contains conversion functions to convert Go to C strings and vice versa
Also: opaque data (behind void *) to []byte
// Go string to C string; result must be freed with C.free() func C.CString(string) *C.char // C string to Go string func C.GoString(*C.char) string // C string, length to Go string func C.GoStringN(*C.char, C.int) string // C pointer, length to Go []byte func C.GoBytes(unsafe.Pointer, C.int) []byte
Write a Cgo program that reads the hostname from the libc function gethostname
and prints it to the console.
The corresponding C function is defined in unistd.h
and can be used as follows:
#include<unistd.h> #include<stdio.h> int main() { char buffer[256]; gethostname(buffer, 256); printf("%s\n", buffer); return 0; }
Write a Cgo program that creates a new SQLITE database file and executes the following statements:
create table if not exists students (id integer not null primary key autoincrement, name text); insert into students (name) values ('Your Name'); insert into students (name) values ('Another Name');
Do not use any GitHub libraries, but write your own Cgo wrapper.
Verify the content of the database using any SQLITE inspector (e.g. IntelliJ or https://sqlitebrowser.org/).
github.com/s-macke/concepts-of-programming-languages/blob/master/docs/exercises/Exercise8.md
15#include <stdio.h> #include <stdlib.h> #include <sqlite3.h> // compile with gcc sqlite.c -lsqlite3 void Exec(sqlite3 *db, char *sql) { int rc = sqlite3_exec(db, sql, NULL, NULL, NULL); if (rc != SQLITE_OK) { printf("sqlite3_open returned code %i", rc); exit(1); } } int main() { sqlite3 *db; int rc = sqlite3_open("test.db", &db); if (rc != SQLITE_OK) { printf("sqlite3_open returned code %i", rc); return 1; } Exec(db, "create table if not exists students (id integer not null primary key autoincrement, name text);"); Exec(db, "insert into students (name) values ('Your Name');"); Exec(db, "insert into students (name) values ('Another Name');"); sqlite3_close(db); return 0; }
The packages os and runtime encapsulate platform dependent APIs
// Copyright 2018 Johannes Weigend // Licensed under the Apache License, Version 2.0 package main import ( "fmt" "os" ) func main() { pid := os.Getpid() fmt.Println("process id: ", pid) }
func cleanup() { fmt.Println("cleanup") } func main() { c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c cleanup() os.Exit(1) }() for { fmt.Println("sleeping...") time.Sleep(10 * time.Second) } }
Access to the standard I/O streams
os.Stdin os.Stdout os.Stderr
Functions for executing processes (os/exec)
cmd := exec.Command(path, args)
Access to the environment
os.Getenv("FOO") os.Setenv("FOO", "1")
Access to files, directories, pipes, ...
f, err := os.Open("/tmp/dat") // f.Read() / f.Write() / f.Seek()
Package runtime contains operations that interact with Go's runtime system, such as functions to control goroutines. It also includes the low-level type information used by the reflect package
func main() { println("OS: ", runtime.GOOS) println("ARCH: ", runtime.GOARCH) println("ROOT: ", runtime.GOROOT()) println("Number of CPUs: ", runtime.NumCPU()) println("Current number of goroutines: ", runtime.NumGoroutine()) var rtm runtime.MemStats runtime.ReadMemStats(&rtm) rtmAsBytes, _ := json.MarshalIndent(rtm, "", " ") fmt.Println(string(rtmAsBytes)) }
//go:build linux || darwin // +build linux darwin // Copyright 2018 Johannes Weigend, Johannes Siedersleben // Licensed under the Apache License, Version 2.0 package main import ( "fmt" "golang.org/x/sys/unix" ) func main() { pid, _, _ := unix.Syscall(unix.SYS_GETPID, 0, 0, 0) fmt.Println("process id: ", pid) }
func main() { path := "mydir" dir, _ := syscall.BytePtrFromString(path) mode := 0777 _, _, e := unix.Syscall(unix.SYS_MKDIR, uintptr(unsafe.Pointer(dir)), uintptr(mode), 0) if e != 0 { log.Fatal("Error code", e) } }
github.com/s-macke/concepts-of-programming-languages/blob/master/src/system/fuse/main.go
An inode (index node) descripes a file-system object such as a file or directory.
27# mkdir -p /home/user/jail # chroot /home/user/jail /bin/bash => chroot: failed to run command ‘/bin/bash’: No such file or directory
Change Root can not start because there is no /bin/bash inside the new root!
The solution is to copy bash inclusive dependent libraries inside the directory jail tree
$ mkdir /home/user/jail/bin $ cp /bin/bash /home/user/jail/bin
List dependent libraries for /bin/bash
ldd /bin/bash
linux-vdso.so.1 => (0x00007fff11bff000) libtinfo.so.5 => /lib64/libtinfo.so.5 (0x0000003728800000) libdl.so.2 => /lib64/libdl.so.2 (0x0000003d56400000) libc.so.6 => /lib64/libc.so.6 (0x0000003d56800000) /lib64/ld-linux-x86-64.so.2 (0x0000003d56000000)
Copy libraries to jail root
mkdir /home/user/jail/lib64 $ cp /lib64/{libtinfo.so.5,libdl.so.2,libc.so.6,ld-linux-x86-64.so.2} /home/user/jail/lib64
Start /bin/bash with new root
# chroot /home/user/jail # pwd => / # ls => /bin, /lib64
The bash shell can only access the jail file system!
34
Resource limiting
groups can be set to not exceed a configured memory limit, which also includes the file system cache
Prioritization
some groups may get a larger share of CPU utilization or disk I/O throughput
Accounting
measures a group's resource usage, which may be used, for example, for billing purposes
Control
freezing groups of processes, their checkpointing and restarting
Examples of resource names that can exist in multiple spaces:
First we need a program to check our changes in visibility
stats.go
func main() { fmt.Println("User ID: ", os.Getuid()) fmt.Println("Group ID: ", os.Getgid()) fmt.Println("Process Id: ", os.Getpid()) PrintHostname("Current") /* syscall.Sethostname([]byte("container")) PrintHostname("New") */ } func PrintHostname(pre string) { hostname, _ := os.Hostname() fmt.Println(pre, "Hostname: ", hostname) }
Second, we need a way to run the program inside go
func main() { cmd := exec.Command("./stats") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { panic(err) } }
we just fordward
to the default terminal streams
39cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUSER, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: os.Getuid(), Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: os.Getgid(), Size: 1, }, }, } err := cmd.Run() if err != nil { panic(err) } }
Cloneflags: syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER | syscall.CLONE_NEWUTS, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: os.Getuid(), Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: os.Getgid(), Size: 1, }, }, } err := cmd.Run() if err != nil { panic(err) } }
The next changes can't be made before startup, but have to be done in the started process.
One way to do this is to run itself, but with different parameters.
The code shows the main functionality of the "docker run" command:
// go run main.go run <cmd> <args> func main() { switch os.Args[1] { case "run": run() case "child": child() default: panic("help") } }
The run() function starts a new process (a clone) with a unique namespace
The child() function function is called in the new process and configures the new namespace (setHostname ...)
func run() { log.Printf("Running %v \n", os.Args[1:]) cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{ // Does not compile on OSX Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, Unshareflags: syscall.CLONE_NEWNS, } must(cmd.Run()) }
func child() { log.Printf("Running %v \n", os.Args[1:]) cg() // Initialize CGroup limits cmd := exec.Command(os.Args[2], os.Args[3:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // We are in an independent clone -> Setting hostname is safe! must(syscall.Sethostname([]byte("container"))) must(syscall.Chroot("/opt/alpinefs")) // container local file system (alpine linux) must(os.Chdir("/")) must(syscall.Mount("proc", "proc", "proc", 0, "")) // support for the ps command must(cmd.Run()) must(syscall.Unmount("proc", 0)) }
func cg() { cgroups := "/sys/fs/cgroup/" pids := filepath.Join(cgroups, "pids") os.Mkdir(filepath.Join(pids, "container"), 0755) must(ioutil.WriteFile(filepath.Join(pids, "container/pids.max"), []byte("20"), 0700)) // Removes the new cgroup in place after the container exits must(ioutil.WriteFile(filepath.Join(pids, "container/notify_on_release"), []byte("1"), 0700)) must(ioutil.WriteFile(filepath.Join(pids, "container/cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0700)) ... }
www.youtube.com/watch?v=Utf-A4rODH8
47github.com/s-macke/concepts-of-programming-languages/blob/master/docs/exercises/Exercise8.md
49