Panoramica di alto livello su come printf () funziona con il sistema operativo Windows

2

Ho fatto questa domanda su un canale IRC, purtroppo sto girando in tondo.

Sto puntando a una panoramica di alto livello (ma con alcuni dettagli tecnici se necessario) su come una funzione come printf () da stdio.h "parla" al sistema operativo Windows.

Conosco un po 'di MSVCRT.dll, bit dell'API di Windows come il modo in cui comunica con kernel32.dll che va all'API nativa in ntdll.dll. Penso che VisualStudio utilizzi msvcrt100.dll per la libreria standard C ...

Qualcuno ha raccomandato di usare una libreria standard C open source per capire cosa succede sotto il cofano. Oltre a non sapere come usarne uno nel mio progetto Visual Studio, non saprei ancora come questo comunica con il sistema operativo.

La mia terribile comprensione che manca di diversi passaggi è la seguente:

1) Syntax of printf() is checked against header stdio.h

2) When program is run it uses msvcrt100 for printf

3) msvcrt100 then loads the necessary Windows library such as kernel32

4) Kernel32 then passes it onto ntdll.dll

Questa domanda si concentra su C, ma se C ++ è lo stesso sentiti libero di postare.

Solo Windows.

    
posta user5623335 30.05.2018 - 13:27
fonte

3 risposte

4

Sebbene i dettagli specifici varino tra i sistemi operativi, probabilmente vorrai iniziare con una comprensione di

  • un formato file oggetto (.obj), creato dal compilatore / assemblatore e
  • un formato di file eseguibile (.exe), formato dal linker
  • un formato di file dll, anch'esso formato (e consumato) dal linker

Questi formati di file (basati su disco) contengono sezioni per codice macchina, valori iniziali del programma, rilocazioni e una tabella dei simboli.

Prima che il codice possa essere eseguito dall'hardware, tutti i valori simbolicamente descritti (vale a dire main , printf ) devono essere completamente risolti in posizioni di memoria.

Il compilatore (e l'assemblatore) hanno informazioni incomplete al momento della compilazione circa le posizioni di memoria finali di vari pezzi di codice e dati - quindi, condividono queste informazioni nel file oggetto simbolicamente piuttosto che come indirizzi di memoria finali - questo è dove riposizionamenti e tabella dei simboli entrano in gioco.

La tabella dei simboli ha sia importazioni che esportazioni: le esportazioni assegnano i nomi delle stringhe agli offset all'interno delle sezioni (codice / dati) del file oggetto; mentre le importazioni sono associate a soli nomi di stringhe (esterni per una risoluzione successiva).

Le rilocazioni dicono al consumatore del file oggetto come & dove aggiustare il codice macchina e i dati, una volta noti gli indirizzi di memoria dei simboli; in tal modo, in genere le rilocazioni hanno riferimenti alle voci nella tabella dei simboli (fanno riferimento anche alle sezioni nel modulo oggetto ...).

Un file .exe per un programma ha una posizione speciale main che è contrassegnata come il punto di ingresso nelle intestazioni per l'eseguibile. Un file .exe per un programma ha anche tutti i riferimenti esterni risolti, in un modo o nell'altro - tuttavia, il linker che produce il file eseguibile continua a non avere informazioni complete sugli indirizzi di memoria di tutti i simboli, come in genere non accade fino a quando tempo di caricamento Quindi, il file .exe ha ancora riposizionamenti e una tabella dei simboli.

L'exe combina tutti i file .obj in un singolo file: le sezioni di codice sono concatenate, così come le sezioni di dati. Rispetto al file .obj, alcune delle revoche del file obj possono essere risolte e quindi rimosse dal file .exe; altre delocalizzazioni possono avere la loro forma semplificata (facendo riferimento al codice o alla sezione dati piuttosto che riferendosi a particolari simboli - questo rende più breve la voce di trasferimento).

In genere il caricatore del sistema operativo determina in definitiva le posizioni delle sezioni di codice e dati e questo completa l'assegnazione dell'indirizzo di memoria ai simboli, il che significa che ora è possibile eseguire qualsiasi riposizionamento in sospeso.

Un file DLL (.dll) è come un file .exe tranne per il fatto che ha esportazioni specifiche. Il file .exe creato dal linker potrebbe contenere riferimenti a file .dll. Questi riferimenti vengono letti da un caricatore di collegamento dinamico, in modo che l'operazione di caricamento di un file .exe richieda anche il caricamento di un file .dll, che può richiedere ulteriori file dll aggiuntivi da caricare. Tutti loro sono incrociati l'uno con l'altro secondo le loro voci di trasferimento.

L'hardware quindi esegue istruzioni di codice macchina come call printf , dove il riferimento a printf è specificato dalla sua posizione di memoria. Ci sono molti approcci, alcuni che coinvolgono tabelle di salto o sequenze di codice di diverse istruzioni; tuttavia, è sufficiente dire che durante l'esecuzione del codice macchina, l'hardware vede / conosce solo gli indirizzi di memoria e non i nomi dei simboli. Tutti i riferimenti simbolici vengono infine risolti in indirizzi di memoria dal sistema di riposizionamenti e voci della tabella dei simboli.

printf , quando appropriato (ad es. buffer locale pieno), alla fine invocherà una chiamata di sistema di qualche tipo a eseguire i / o come il lavaggio del buffer locale su disco o dispositivo. Una chiamata di sistema è un modo per un processo utente di richiedere un'operazione del sistema operativo. Le chiamate di sistema sono simili alle normali chiamate ad eccezione del fatto che il chiamante si trova nel processo utente e il chiamato è un punto di ingresso del sistema operativo. Le chiamate di sistema forniscono un metodo controllato di aumentare il privilegio (dall'utente al kernel) secondo necessità per fornire accesso ai dispositivi condivisi. Le chiamate di sistema non utilizzano l'indirizzo di memoria, ma ogni chiamata di sistema è associata a un indice intero semplice - questo consente a .exe e .dll di chiamare nel sistema operativo per non essere a conoscenza degli indirizzi di memoria del kernel.

    
risposta data 30.05.2018 - 16:44
fonte
1

Ci sono due modi in cui un programma Windows interagisce con una DLL.

Il metodo normale è che parte dell'avvio del processo coinvolge finestre che mappano il contenuto della DLL nello spazio di indirizzamento del processo e associano gli indirizzi (caricati) delle funzioni esportate (e dati) con gli obiettivi di chiamata (predefiniti) in il codice del programma. Nel momento in cui main è iniziato, tutto si comporta come se le funzioni della dll fossero in coda per tutto il tempo.

L'altro metodo è che il caricamento della DLL viene ritardato fino a dopo l'avvio e vengono richiesti i puntatori alle funzioni della DLL. Le due funzioni importanti qui sono LoadLibrary e GetProcAddress .

    
risposta data 30.05.2018 - 15:23
fonte
0

Probabilmente vorrai leggere la documentazione per WriteConsole . È la funzione nativa di Windows che corrisponde più strettamente a printf . Come puoi vedere, richiede una stringa formattata - funziona più come puts . Potrebbe essere necessario chiamare prima sprintf , prima di chiamare WriteConsole .

C ++ ha una divisione simile di funzionalità. std::cout chiama infine WriteConsole , mentre operator<< esegue la formattazione dei singoli argomenti.

    
risposta data 18.06.2018 - 14:35
fonte

Leggi altre domande sui tag