Syscalls Linux com Zig: Guia Completo para Chamadas de Sistema

Syscalls sao a interface fundamental entre seus programas e o kernel do sistema operacional. Toda operacao que envolve recursos do sistema — abrir arquivos, alocar memoria, criar processos, enviar dados pela rede — passa por uma syscall. Neste primeiro artigo da serie Programacao de Sistemas com Zig, vamos explorar como Zig facilita o uso direto de syscalls Linux, oferecendo uma experiencia muito mais segura e ergonomica do que C.

Este artigo assume que voce ja tem familiaridade com a sintaxe basica de Zig. Se voce esta comecando, confira primeiro a serie Zig para Iniciantes.

O Que Sao Syscalls?

Quando um programa em user space precisa interagir com hardware ou recursos gerenciados pelo kernel, ele nao pode fazer isso diretamente. Em vez disso, ele faz uma chamada de sistema (syscall), que e uma requisicao ao kernel para executar uma operacao privilegiada.

O fluxo e o seguinte:

  1. O programa coloca o numero da syscall em um registrador (rax no x86_64)
  2. Os argumentos sao colocados em registradores especificos (rdi, rsi, rdx, r10, r8, r9)
  3. A instrucao syscall e executada, transferindo controle ao kernel
  4. O kernel executa a operacao e retorna o resultado em rax

Em C, voce tipicamente usa wrappers da libc como read(), write(), open(). Em Zig, voce tem acesso direto as syscalls atraves do modulo std.os.linux e tambem pode usar a standard library de forma mais ergonomica.

Syscalls Diretas em Zig

Zig permite invocar syscalls diretamente sem depender da libc. Isso e particularmente util para sistemas embarcados, binarios estaticos minimos ou quando voce precisa de controle total.

Exemplo: write syscall direta

const std = @import("std");
const linux = std.os.linux;

pub fn main() void {
    const mensagem = "Ola direto do kernel!\n";
    // syscall write: fd=1 (stdout), buffer, tamanho
    const resultado = linux.syscall3(
        .write,
        1, // file descriptor 1 = stdout
        @intFromPtr(mensagem.ptr),
        mensagem.len,
    );

    if (resultado < 0) {
        // Erro na syscall
        std.debug.print("Erro na syscall write: {d}\n", .{resultado});
    }
}

Observe que estamos chamando linux.syscall3 diretamente, passando o numero da syscall (.write) e seus tres argumentos. O “3” em syscall3 indica o numero de argumentos.

Entendendo os Numeros de Syscall

Zig define todas as syscalls Linux como um enum em std.os.linux.SYS. Voce pode usar nomes simbolicos em vez de numeros magicos:

const linux = std.os.linux;

// Em vez de usar numeros magicos:
// const SYS_write = 1;  // Estilo C

// Zig usa um enum tipado:
const syscall_write = linux.SYS.write;    // = 1 no x86_64
const syscall_read = linux.SYS.read;      // = 0 no x86_64
const syscall_open = linux.SYS.open;      // = 2 no x86_64
const syscall_close = linux.SYS.close;    // = 3 no x86_64

Isso torna o codigo portavel entre arquiteturas, ja que os numeros de syscall podem variar entre x86_64, aarch64, etc.

A Standard Library: O Caminho Recomendado

Embora syscalls diretas sejam educativas, para codigo de producao a standard library de Zig oferece wrappers muito mais seguros e ergonomicos. Esses wrappers cuidam do tratamento de erros, conversao de tipos e compatibilidade entre plataformas.

Abrindo e Lendo Arquivos

const std = @import("std");

pub fn main() !void {
    // Abrir arquivo para leitura
    const arquivo = try std.fs.cwd().openFile("/etc/hostname", .{});
    defer arquivo.close();

    // Ler conteudo em um buffer
    var buffer: [1024]u8 = undefined;
    const bytes_lidos = try arquivo.read(&buffer);

    const conteudo = buffer[0..bytes_lidos];
    std.debug.print("Hostname: {s}", .{conteudo});
}

Compare com o equivalente em C:

// Versao C - mais verbosa e propensa a erros
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("/etc/hostname", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char buffer[1024];
    ssize_t n = read(fd, buffer, sizeof(buffer));
    if (n == -1) {
        perror("read");
        close(fd);  // Facil esquecer!
        return 1;
    }

    buffer[n] = '\0';  // Facil esquecer!
    printf("Hostname: %s", buffer);
    close(fd);  // Facil esquecer!
    return 0;
}

Em Zig, defer arquivo.close() garante que o arquivo sera fechado nao importa o que aconteca. O operador try propaga erros automaticamente. Nao ha risco de esquecer o close() ou de nao verificar erros.

Escrevendo em Arquivos

const std = @import("std");

pub fn main() !void {
    // Criar ou truncar arquivo
    const arquivo = try std.fs.cwd().createFile("saida.txt", .{});
    defer arquivo.close();

    // Escrever conteudo
    try arquivo.writeAll("Primeira linha\n");
    try arquivo.writeAll("Segunda linha\n");

    // Usar writer formatado
    const writer = arquivo.writer();
    try writer.print("Valor: {d}\n", .{42});
    try writer.print("Data: {s}\n", .{"2026-02-21"});
}

Syscalls Essenciais para Programadores Zig

Vamos explorar as syscalls mais importantes que todo programador de sistemas precisa conhecer.

open, read, write, close — O Quarteto Fundamental

Estas quatro syscalls formam a base de toda operacao de I/O no Linux. Tudo e um arquivo no Unix: dispositivos, pipes, sockets e arquivos regulares sao todos manipulados com essas syscalls.

const std = @import("std");
const posix = std.posix;

pub fn main() !void {
    // open: abre um file descriptor
    const fd = try posix.open("/tmp/teste_syscall.txt", .{
        .ACCMODE = .WRONLY,
        .CREAT = true,
        .TRUNC = true,
    }, 0o644);
    defer posix.close(fd);

    // write: escreve bytes no file descriptor
    const dados = "Escrito via posix.open!\n";
    const bytes_escritos = try posix.write(fd, dados);
    std.debug.print("Bytes escritos: {d}\n", .{bytes_escritos});
}

mmap — Mapeamento de Memoria

mmap e uma das syscalls mais poderosas do Linux. Ela mapeia arquivos ou memoria anonima diretamente no espaco de enderecamento do processo.

const std = @import("std");
const posix = std.posix;

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("dados.bin", .{});
    defer arquivo.close();

    const stat = try arquivo.stat();
    const tamanho = stat.size;

    // Mapear arquivo na memoria
    const mapeado = try posix.mmap(
        null,
        tamanho,
        posix.PROT.READ,
        .{ .TYPE = .SHARED },
        arquivo.handle,
        0,
    );
    defer posix.munmap(mapeado);

    // Agora podemos acessar o arquivo como um slice de bytes
    const bytes: []const u8 = @as([*]const u8, @ptrCast(mapeado))[0..tamanho];
    std.debug.print("Primeiros 100 bytes: {s}\n", .{bytes[0..@min(100, bytes.len)]});
}

mmap e extremamente eficiente para arquivos grandes porque o kernel carrega as paginas sob demanda (lazy loading), e multiplos processos podem compartilhar a mesma memoria fisica.

ioctl — Controle de Dispositivos

ioctl e a syscall “coringa” do Linux, usada para operacoes especificas de dispositivos que nao se encaixam no modelo read/write.

const std = @import("std");
const posix = std.posix;
const linux = std.os.linux;

const TIOCGWINSZ = 0x5413;

const Winsize = extern struct {
    ws_row: u16,
    ws_col: u16,
    ws_xpixel: u16,
    ws_ypixel: u16,
};

pub fn obterTamanhoTerminal() !Winsize {
    var ws: Winsize = undefined;
    const resultado = linux.ioctl(
        std.io.getStdOut().handle,
        TIOCGWINSZ,
        @intFromPtr(&ws),
    );
    if (resultado != 0) return error.IoctlFailed;
    return ws;
}

pub fn main() !void {
    const tamanho = try obterTamanhoTerminal();
    std.debug.print("Terminal: {d} linhas x {d} colunas\n", .{
        tamanho.ws_row,
        tamanho.ws_col,
    });
}

Tratamento de Erros em Syscalls

Uma das maiores vantagens de Zig sobre C para programacao de sistemas e o tratamento de erros. Em C, voce precisa verificar manualmente o valor de retorno de cada syscall e consultar errno. Em Zig, o sistema de error unions torna isso natural e impossivel de esquecer.

Padrao em C (propenso a erros)

int fd = open("arquivo.txt", O_RDONLY);
if (fd == -1) {
    // Programador pode esquecer esta verificacao
    perror("open");
    return 1;
}
// Se esquecer o if, o programa continua com fd = -1

Padrao em Zig (seguro por design)

const std = @import("std");

pub fn main() !void {
    // Se open falhar, o erro e propagado automaticamente com try
    const arquivo = try std.fs.cwd().openFile("arquivo.txt", .{});
    defer arquivo.close();

    // Ou trate o erro explicitamente
    const arquivo2 = std.fs.cwd().openFile("outro.txt", .{}) catch |err| {
        std.debug.print("Erro ao abrir: {}\n", .{err});
        return;
    };
    defer arquivo2.close();
}

O compilador Zig exige que voce lide com cada possivel erro. Voce nao pode simplesmente ignorar o retorno de uma funcao que pode falhar. Isso elimina uma classe inteira de bugs que sao comuns em programas C.

Criando Wrappers Seguros

Para projetos maiores, e uma boa pratica criar wrappers que adicionam contexto e logging as operacoes de syscall.

const std = @import("std");

const SysError = error{
    PermissaoNegada,
    ArquivoNaoEncontrado,
    SemEspaco,
    ErroDesconhecido,
};

pub fn lerArquivoCompleto(
    allocator: std.mem.Allocator,
    caminho: []const u8,
) ![]u8 {
    const arquivo = std.fs.cwd().openFile(caminho, .{}) catch |err| {
        std.log.err("Falha ao abrir '{s}': {}", .{ caminho, err });
        return switch (err) {
            error.FileNotFound => SysError.ArquivoNaoEncontrado,
            error.AccessDenied => SysError.PermissaoNegada,
            else => SysError.ErroDesconhecido,
        };
    };
    defer arquivo.close();

    const conteudo = try arquivo.readToEndAlloc(allocator, 10 * 1024 * 1024);
    std.log.info("Lido {d} bytes de '{s}'", .{ conteudo.len, caminho });
    return conteudo;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const conteudo = try lerArquivoCompleto(allocator, "/etc/os-release");
    defer allocator.free(conteudo);

    std.debug.print("{s}\n", .{conteudo});
}

Comparacao: Zig vs C para Syscalls

AspectoCZig
Verificacao de errosManual, facil de esquecerObrigatoria pelo compilador
Limpeza de recursosManual (close, free)Automatica com defer
PortabilidadeDepende da libcPode compilar sem libc
Seguranca de tiposFraca (void*, int para tudo)Forte (enums, error unions)
Buffer overflowsFaceis de causarVerificacao em runtime
Cross-compilationComplexa (toolchains)zig build -Dtarget=...

Exercicios Praticos

Exercicio 1: Informacoes do Sistema

Crie um programa que leia e exiba o conteudo de /proc/cpuinfo e /proc/meminfo, extraindo o modelo do processador e a memoria total.

Exercicio 2: Copia de Arquivos

Implemente uma funcao que copie um arquivo para outro usando buffers de 4096 bytes, exibindo o progresso durante a copia.

Exercicio 3: Listagem de Diretorios

Use a standard library para listar todos os arquivos de um diretorio, mostrando nome, tamanho e permissoes.


Proximo Artigo

No proximo artigo, vamos nos aprofundar em operacoes de file system, explorando manipulacao de diretorios, permissoes, file locking e memory-mapped files.

Conteudo Relacionado


Tem duvidas sobre syscalls em Zig? Deixe um comentario ou entre em contato com a comunidade Zig Brasil.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.