73 votes

C/C++ des définitions de fonction sans assemblage

J'ai toujours pensé que les fonctions telles que printf() sont, dans la dernière étape, définie à l'aide d'assembly en ligne. Que de profondeur dans les entrailles de stdio.h est enterré asm code qui raconte en fait l'UC quoi faire. Par exemple, dans le dos, je me souviens qu'il a été mis en œuvre par le premier moving le début de la chaîne à un emplacement mémoire ou registre et que l'appel d'un intterupt.

Cependant, depuis la version 64 bits de Visual Studio ne prend pas en charge assembleur en ligne, il m'a fait me demander comment il peut ne pas assembler les fonctions définies à tous en C/C++. Comment fonctionne une fonction de la bibliothèque comme printf() mise en œuvre en C/C++ sans utiliser de code en langage assembleur? Ce qui s'exécute en fait le droit d'interruption logicielle? Merci.

132voto

HostileFork Points 14697

Vous avez évidemment raison que le caoutchouc est pour répondre à la route à certains point. Mais il y a beaucoup de couches à passer avant que vous pouvez trouver! Il semble que vous avez quelques idées préconçues basées sur le DOS jours, et ce n'est pas aussi pertinente.

Il y a eu quelques bons points ici, mais personne ne l'a lié à la précision des diables dans les détails de la source. Donc, afin de vous faire suffisamment désolé que vous avez demandé :) j'ai fait une approfondie trace de l' printf histoire de GNU libc et Linux..en essayant de ne pas la main-d'onde à propos de l'une de ces étapes. Dans le processus, j'ai apporté quelques-unes de mes connaissances à jour (ATTENTION: Ce n'est pas pour facilement s'ennuyer!):

(Le lien d'origine est http://blog.hostilefork.com/where-printf-rubber-meets-road/, et il sera maintenu. Mais pour éviter lien pourrir ici, c'est le contenu mis en cache.)

Premiers Pas

Nous allons bien sûr de commencer avec le prototype pour le printf, qui est définie dans le fichier libc/libio/stdio.h

extern int printf (__const char *__restrict __format, ...);

Vous ne trouverez pas le code source d'une fonction appelée printf, cependant. Au lieu de cela, dans le fichier /libc/stdio-common/printf.c vous y trouverez un peu de code associé à une fonction appelée __printf:

int __printf (const char *format, ...)
{
    va_list arg;
    int done;

    va_start (arg, format);
    done = vfprintf (stdout, format, arg);
    va_end (arg);

    return done;
}

Une macro dans le même fichier met en place une association pour que cette fonction est définie comme un alias pour le non-souligné printf:

ldbl_strong_alias (__printf, printf);

Il est logique que printf serait une fine couche qui appelle vfprintf avec stdout. En effet, la viande de la mise en forme du travail est fait dans vfprintf, que vous trouverez dans libc/stdio-common/vfprintf.c. C'est assez long, mais vous pouvez voir qu'il est encore dans C!

Plus profondément vers le Bas le Trou de Lapin...

vfprintf mystérieusement appels outchar et outstring, qui sont bizarre les macros définies dans le même fichier:

#define outchar(Ch) \
   do \
   { \
       register const INT_T outc = (Ch); \
       if (PUTC (outc, s) == EOF || done == INT_MAX) \
       { \
            done = -1; \
            goto all_done; \
       } \
       ++done; \
   } \
   while (0)

Esquiver la question de savoir pourquoi il est si bizarre, on voit que c'est dépendante de l'énigmatique PUTC, également dans le même fichier:

#define PUTC(C, F) IO_putwc_unlocked (C, F)

Lorsque vous arrivez à la définition de l' IO_putwc_unlocked en libc/libio/libio.h, vous pourriez commencer à penser que vous n'avez plus d'attention à la manière de printf œuvres:

#define _IO_putwc_unlocked(_wch, _fp) \
   (_IO_BE ((_fp)->_wide_data->_IO_write_ptr \
        >= (_fp)->_wide_data->_IO_write_end, 0) \
        ? __woverflow (_fp, _wch) \
        : (_IO_wint_t) (*(_fp)->_wide_data->_IO_write_ptr++ = (_wch)))

Mais en dépit d'être un peu dur à lire, c'est juste faire le tampon de sortie. Si il y a assez de place dans le pointeur de fichier de la mémoire tampon, puis il va juste coller le personnage en lui... mais si pas, il appelle __woverflow. Comme la seule option lorsque vous n'avez plus de mémoire tampon est de rincer à l'écran (ou quel que soit le périphérique de votre pointeur de fichier représente), on peut espérer trouver l'incantation magique.

Vtables en C?

Si vous avez deviné que nous allons hop à travers un autre frustrant niveau d'indirection, vous auriez raison. Regardez dans la libc/libio/wgenops.c et vous trouverez la définition de la __woverflow:

wint_t 
__woverflow (f, wch)
    _IO_FILE *f;
    wint_t wch;
{
    if (f->_mode == 0)
        _IO_fwide (f, 1);
    return _IO_OVERFLOW (f, wch);
}

Fondamentalement, les pointeurs de fichiers sont mis en œuvre dans la GNU standard de la bibliothèque en tant qu'objets. Ils ont données des membres, mais aussi les membres de fonction que vous pouvez appeler avec des variations du SAUT de la macro. Dans le fichier libc/libio/libioP.h vous trouverez un peu de documentation sur cette technique:

/* THE JUMPTABLE FUNCTIONS.

 * The _IO_FILE type is used to implement the FILE type in GNU libc,
 * as well as the streambuf class in GNU iostreams for C++.
 * These are all the same, just used differently.
 * An _IO_FILE (or FILE) object is allows followed by a pointer to
 * a jump table (of pointers to functions).  The pointer is accessed
 * with the _IO_JUMPS macro.  The jump table has a eccentric format,
 * so as to be compatible with the layout of a C++ virtual function table.
 * (as implemented by g++).  When a pointer to a streambuf object is
 * coerced to an (_IO_FILE*), then _IO_JUMPS on the result just
 * happens to point to the virtual function table of the streambuf.
 * Thus the _IO_JUMPS function table used for C stdio/libio does
 * double duty as the virtual function table for C++ streambuf.
 *
 * The entries in the _IO_JUMPS function table (and hence also the
 * virtual functions of a streambuf) are described below.
 * The first parameter of each function entry is the _IO_FILE/streambuf
 * object being acted on (i.e. the 'this' parameter).
 */

Ainsi, lorsque nous trouvons IO_OVERFLOW en libc/libio/genops.c, nous trouver, c'est une macro qui appelle un "1-parameter" __overflow méthode sur le pointeur de fichier:

#define IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

Le saut de tables pour les différents pointeur de fichier types sont dans la libc/libio/fileops.c

const struct _IO_jump_t _IO_file_jumps =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, INTUSE(_IO_file_finish)),
  JUMP_INIT(overflow, INTUSE(_IO_file_overflow)),
  JUMP_INIT(underflow, INTUSE(_IO_file_underflow)),
  JUMP_INIT(uflow, INTUSE(_IO_default_uflow)),
  JUMP_INIT(pbackfail, INTUSE(_IO_default_pbackfail)),
  JUMP_INIT(xsputn, INTUSE(_IO_file_xsputn)),
  JUMP_INIT(xsgetn, INTUSE(_IO_file_xsgetn)),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, INTUSE(_IO_file_doallocate)),
  JUMP_INIT(read, INTUSE(_IO_file_read)),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, INTUSE(_IO_file_seek)),
  JUMP_INIT(close, INTUSE(_IO_file_close)),
  JUMP_INIT(stat, INTUSE(_IO_file_stat)),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)

Il y a également un #define, ce qui équivaut_IO_new_file_overflow avec _IO_file_overflow, et le premier est défini dans le même fichier source. (Note: INTUSE est juste une macro qui marque les fonctions qui sont à usage interne, cela ne veut pas dire quelque chose comme "cette fonction utilise une interruption")

Sommes-nous encore là?!

Le code source pour _IO_new_file_overflow un groupe de plus de la mémoire tampon de la manipulation, mais il n'appelez _IO_do_flush:

#define _IO_do_flush(_f) \
    INTUSE(_IO_do_write)(_f, (_f)->_IO_write_base, \
        (_f)->_IO_write_ptr-(_f)->_IO_write_base)

Nous sommes maintenant à un point où _IO_do_write est probablement là où le caoutchouc répond effectivement à la route: un tampon, réels, directs écrire à un périphérique d'e/S. Au moins on peut l'espérer! Elle est représentée par une macro pour _IO_new_do_write et nous avons ceci:

static
_IO_size_t
new_do_write (fp, data, to_do)
     _IO_FILE *fp;
     const char *data;
     _IO_size_t to_do;
{
  _IO_size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       is not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      _IO_off64_t new_pos
    = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
    return 0;
      fp->_offset = new_pos;
    }
  count = _IO_SYSWRITE (fp, data, to_do);
  if (fp->_cur_column && count)
    fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data,
                         count) + 1;
  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
  fp->_IO_write_end = (fp->_mode <= 0
               && (fp->_flags & (_IO_LINE_BUF+_IO_UNBUFFERED))
               ? fp->_IO_buf_base : fp->_IO_buf_end);
  return count;
}

Malheureusement nous nous sommes coincés à nouveau... _IO_SYSWRITE est en train de faire le travail:

/* The 'syswrite' hook is used to write data from an existing buffer
   to an external file.  It generalizes the Unix write(2) function.
   It matches the streambuf::sys_write virtual function, which is
   specific to this implementation. */
typedef _IO_ssize_t (*_IO_write_t) (_IO_FILE *, const void *, _IO_ssize_t);
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
#define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)

Donc, à l'intérieur de la do_write nous appelons la méthode d'écriture sur le pointeur de fichier. Nous savons de par notre saut tableau ci-dessus qui est mappé à _IO_new_file_write, alors à quoi ça sert?

_IO_ssize_t
_IO_new_file_write (f, data, n)
     _IO_FILE *f;
     const void *data;
     _IO_ssize_t n;
{
  _IO_ssize_t to_do = n;
  while (to_do > 0)
    {
      _IO_ssize_t count = (__builtin_expect (f->_flags2
                         & _IO_FLAGS2_NOTCANCEL, 0)
               ? write_not_cancel (f->_fileno, data, to_do)
               : write (f->_fileno, data, to_do));
      if (count < 0)
    {
      f->_flags |= _IO_ERR_SEEN;
      break;
        }
      to_do -= count;
      data = (void *) ((char *) data + count);
    }
  n -= to_do;
  if (f->_offset >= 0)
    f->_offset += n;
  return n;
}

Maintenant, il appelle à écrire! Bien où est la mise en œuvre pour cela? Vous trouverez écrire en libc/posix/unistd.h:

/* Write N bytes of BUF to FD.  Return the number written, or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;

(Remarque: __wur est une macro pour __attribute__ ((__warn_unused_result__)))

Des fonctions Générées à Partir d'une Table

C'est seulement un prototype pour l'écriture. Vous ne trouverez pas d'écrire.fichier c pour Linux dans la GNU de la bibliothèque standard. Au lieu de cela, vous trouverez une plate-forme spécifique méthodes de connexion à l'OS la fonction d'écriture dans diverses manières, le tout dans la libc/sysdeps/ répertoire.

Nous allons continuer à suivre ainsi que la façon dont Linux fait. Il y a un fichier appelé sysdeps/unix/syscalls.list qui est utilisé pour générer la fonction d'écriture automatique. Les données pertinentes à partir de la table est la suivante:

File name: write
Caller: "-" (i.e. Not Applicable)
Syscall name: write
Args: Ci:ibn
Strong name: __libc_write
Weak names: __write, write

Pas du tout mystérieux, à l'exception de l' Ci:ibn. Le C signifie "annulable". Le côlon sépare le type de retour de l'argument, les types, et si vous voulez une explication plus approfondie de ce qu'ils signifient, alors vous pouvez voir le commentaire dans le script shell qui génère le code, libc/sysdeps/unix/make-syscalls.sh.

Alors maintenant, nous nous attendons à être en mesure de relier à l'encontre d'une fonction appelée __libc_l'écriture qui est généré par ce script shell. Mais qu'en est-il généré? Code C qui implémente l'écriture via une macro appelée SYS_ify, que vous trouverez dans sysdeps/unix/sysdep.h

#define SYS_ify(syscall_name) __NR_##syscall_name

Ah, le bon vieux jeton-coller :P. Donc, fondamentalement, la mise en œuvre de cette __libc_write devient rien de plus qu'un proxy invocation de la syscall fonction avec un paramètre nommé __NR_write, et les autres arguments.

Là Où Le Trottoir Se Termine...

Je sais que cela a été un voyage fascinant, mais maintenant nous sommes à la fin de la GNU libc. Ce nombre __NR_write est défini par Linux. Pour 32-bit X86 architectures, il vous mènera à l' linux/arch/x86/include/asm/unistd_32.h:

#define __NR_write 4

La seule chose qui reste à regarder, puis, est la mise en œuvre de syscall. Que je peut faire à un certain point, mais pour l'instant je vais juste le point vous sur certaines références pour comment ajouter un appel système pour Linux.

19voto

Macmade Points 27414

Tout d'abord, vous devez comprendre le concept d'anneaux.
Un noyau s'exécute en ring 0, ce qui signifie qu'il a un accès complet à la mémoire et aux opérateurs.
Un programme s'exécute habituellement dans le ring 3. Il a limité l'accès à la mémoire, et ne peut pas utiliser tous les opcodes.

Ainsi, lorsqu'un besoin de logiciel de plus de privilèges (pour l'ouverture d'un fichier, l'écriture d'un fichier, l'allocation de mémoire, etc), il faut qu'il demande au noyau.
Cela peut être fait de plusieurs façons. Les interruptions logicielles, SYSENTER, etc.

Prenons l'exemple des interruptions logicielles, avec la fonction printf ():
1 - Votre logiciel appels à printf().
2 - printf() les processus de votre chaîne, et les arguments, et doit ensuite exécuter une fonction noyau, comme l'écriture dans un fichier ne peut pas être fait dans le ring 3.
3 - printf() génère une interruption logicielle, en la plaçant dans un registre le numéro d'une fonction noyau (dans ce cas, la fonction write() de la fonction).
4 - Le logiciel d'exécution est interrompu, et le pointeur d'instruction se déplace vers le code du noyau. Nous sommes donc maintenant dans le ring 0, dans une fonction noyau.
5 - Le noyau du processus de la demande, l'écriture du fichier (stdout est un descripteur de fichier).
6 - Quand c'est fait, le noyau renvoie au logiciel code, à l'aide de l'instruction iret.
7 - Le logiciel de code continue.

Donc les fonctions de la bibliothèque C standard peuvent être mises en œuvre dans C. Tout ce qu'elle a à faire est de savoir comment appeler le noyau lorsqu'il besoin de plus de privilèges.

5voto

Daniel Genin Points 46

Dans Linux, strace utilitaire vous permet de voir ce système d'appels sont effectués par un programme. Donc, prendre un programme de ce genre


 int main(){
printf("x");
 return 0;
}

Dire, vous le compiler en tant que printx, alors strace printx donne


 execve("./printx", ["./printx"], [/* 49 vars */]) = 0
 brk(0) = 0xb66000
 l'accès("/etc/ld..nohwcap", F_OK) = -1 ENOENT (Aucun fichier ou répertoire)
 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000
 l'accès("/etc/ld..précharge", R_OK) = -1 ENOENT (Aucun fichier ou répertoire)
 open("/etc/ld..cache", O_RDONLY|O_CLOEXEC) = 3
 fstat(3, {st_mode=S_IFREG|0644, st_size=119796, ...}) = 0
 mmap(NULL, 119796, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa6dc0c7000
 fermer(3) = 0
 l'accès("/etc/ld..nohwcap", F_OK) = -1 ENOENT (Aucun fichier ou répertoire)
 open("/lib/x86_64-linux-gnu/libc..6", O_RDONLY|O_CLOEXEC) = 3
 lire(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30\2\0\0\0\0\0"..., 832) = 832
 fstat(3, {st_mode=S_IFREG|0755, st_size=1811128, ...}) = 0
 mmap(NULL, 3925208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa6dbb06000
 mprotect(0x7fa6dbcbb000, 2093056, PROT_NONE) = 0
 mmap(0x7fa6dbeba000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b4000) = 0x7fa6dbeba000
 mmap(0x7fa6dbec0000, 17624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa6dbec0000
 fermer(3) = 0
 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c6000
 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c5000
 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c4000
 arch_prctl(ARCH_SET_FS, 0x7fa6dc0c5700) = 0
 mprotect(0x7fa6dbeba000, 16384, PROT_READ) = 0
 mprotect(0x600000, 4096, PROT_READ) = 0
 mprotect(0x7fa6dc0e7000, 4096, PROT_READ) = 0
 munmap(0x7fa6dc0c7000, 119796) = 0
 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e4000
 write(1, "x", 1) = 1
 exit_group(0) = ?

Le caoutchouc rencontre la route (tri, voir ci-dessous) dans l'avant-dernier appel de la trace: write(1,"x",1x). À ce stade, le contrôle passe de l'utilisateur des terres printx pour le noyau Linux qui gère le reste. write() est une fonction wrapper déclaré en unistd.h


 extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;

La plupart des appels système sont enveloppés dans cette voie. La fonction wrapper, comme son nom l'indique, est un peu plus qu'une mince couche de code qui place les arguments dans le bon registres et puis exécute un logiciel d'interruption 0x80. Le noyau des pièges de l'interruption et le reste est l'histoire. Ou du moins c'est la façon dont il l'habitude de travailler. Apparemment, les frais généraux de l'interruption de piégeage a été très élevé et, comme un précédent post souligné, moderne architectures des processeurs introduite sysenter instructions de montage, qui effectue le même résultat à la vitesse. Cette page Appels Système a un assez bon résumé de la façon dont les appels système de travail.

Je sens que vous allez probablement être un peu déçu par cette réponse, comme l'a I. Clairement, dans un certain sens, c'est un faux fond car il y a encore pas mal de choses qui doivent arriver entre l'appel à l' write() et le point de la carte graphique " frame buffer est effectivement modifié pour rendre la lettre "x" apparaît sur votre écran. Zoom sur le point de contact (pour rester avec le "caoutchouc à l'encontre de la route" analogie) par la plongée dans le noyau est sûr d'être l'éducation si un temps long de l'effort. Je devine que vous avez à voyager à travers plusieurs couches d'abstraction comme le tampon de sortie des ruisseaux, des appareils, etc. Assurez-vous de publier les résultats si vous décidez de suivre cette:)

4voto

Tronic Points 6457

La bibliothèque standard de fonctions sont mises en œuvre sur une plateforme sous-jacente de la bibliothèque (par exemple, UNIX API) et/ou par des appels système (qui sont toujours les fonctions C). Les appels système sont (sur les plates-formes que je connais) en interne mis en œuvre par un appel à une fonction en ligne de l'asm qui met un système de numéro d'appel et les paramètres dans les registres du CPU et déclenche une interruption que le noyau, puis les processus.

Il ya aussi d'autres façons de communiquer avec le matériel en plus de syscalls, mais ces dernières sont souvent indisponibles ou plutôt limitée lors de l'exécution sous un système d'exploitation moderne, ou, au moins, leur permettant nécessite quelques appels. Un périphérique peut être mappé en mémoire, afin que les écritures de certaines adresses de la mémoire (via régulière des pointeurs) le contrôle de l'appareil. Ports d'e/S sont également souvent utilisé et en fonction de l'architecture de ceux-ci sont accessibles par des CPU opcodes ou elles sont, elles aussi, peuvent être mappés en mémoire à des adresses spécifiques.

1voto

Vlad Points 3199

Eh bien, tous les C++ états, sauf le point-virgule et les commentaires finissent par devenir des code machine qui raconte CPU quoi faire. Vous pouvez écrire votre propre fonction printf sans avoir recours à l'assemblée. Les seules opérations qui doivent être écrites à l'assemblée sont l'entrée et la sortie des ports, et des choses que d'activer et de désactiver les interruptions.

Toutefois, l'assemblage est toujours utilisé dans la programmation au niveau du système pour des raisons de performances. Même si l'assembly en ligne n'est pas pris en charge, il n'y a rien qui vous empêche d'écrire un module séparé en assemblée et en le reliant à votre demande.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X