1) le binaire compilé est écrit dans prom / flash yes. USB, série, i2c, jtag, etc. dépend de l'appareil quant à ce qui est pris en charge par cet appareil, non pertinent pour comprendre le processus de démarrage.
2) Ce n'est généralement pas vrai pour un microcontrôleur, le principal cas d'utilisation est d'avoir des instructions en rom / flash et des données en ram. Quelle que soit l'architecture. pour un non-microcontrôleur, votre PC, votre ordinateur portable, votre serveur, le programme est copié de non-volatile (disque) vers RAM puis exécuté à partir de là. Certains microcontrôleurs vous permettent également d'utiliser le ram, même ceux qui prétendent être harvard même s'il semble enfreindre la définition. Il n'y a rien à propos de harvard qui vous empêche de mapper ram dans le côté instruction, il vous suffit d'avoir un mécanisme pour y obtenir les instructions après la mise sous tension (ce qui viole la définition, mais les systèmes harvard devraient le faire pour être utiles autres que comme microcontrôleurs).
3) en quelque sorte.
Chaque unité centrale "démarre" de manière déterministe, telle que conçue. La manière la plus courante est une table vectorielle où l'adresse des premières instructions à exécuter après la mise sous tension se trouve dans le vecteur de réinitialisation, une adresse que le matériel lit puis utilise cette adresse pour commencer à s'exécuter. L'autre manière générale consiste à faire démarrer le processeur sans table vectorielle à une adresse bien connue. Parfois, la puce aura des "sangles", certaines broches que vous pouvez attacher haut ou bas avant de relâcher la réinitialisation, que la logique utilise pour démarrer de différentes manières. Vous devez séparer le processeur lui-même, le cœur du processeur du reste du système. Comprendre le fonctionnement du processeur, puis comprendre que les concepteurs de puce / système ont configuré des décodeurs d'adresses à l'extérieur du processeur afin qu'une partie de l'espace d'adressage du processeur communique avec un flash, et certains avec ram et certains avec périphériques (uart, i2c, spi, gpio, etc.). Vous pouvez prendre ce même noyau cpu si vous le souhaitez et l'envelopper différemment. C'est ce que vous obtenez lorsque vous achetez quelque chose à base de bras ou de mips. les bras et les mips font des cœurs de processeur, qui permettent aux gens d'acheter et d'enrouler leurs propres trucs, pour diverses raisons, ils ne rendent pas ces trucs compatibles d'une marque à l'autre. C'est pourquoi on peut rarement poser une question de bras générique quand il s'agit de quelque chose en dehors du noyau.
Un microcontrôleur tente d'être un système sur une puce, donc sa mémoire non volatile (flash / rom), volatile (sram) et cpu sont tous sur la même puce avec un mélange de périphériques. Mais la puce est conçue en interne de telle sorte que le flash est mappé dans l'espace d'adressage du processeur qui correspond aux caractéristiques de démarrage de ce processeur. Si, par exemple, le processeur a un vecteur de réinitialisation à l'adresse 0xFFFC, il doit y avoir un flash / rom qui répond à cette adresse que nous pouvons programmer via 1), ainsi que suffisamment de flash / rom dans l'espace d'adressage pour les programmes utiles. Un concepteur de puces peut choisir d'avoir 0x1000 octets de flash à partir de 0xF000 afin de satisfaire ces exigences. Et peut-être qu'ils ont mis une certaine quantité de RAM à une adresse inférieure ou peut-être 0x0000, et les périphériques quelque part au milieu.
Une autre architecture de cpu pourrait commencer à s'exécuter à l'adresse zéro, ils devraient donc faire le contraire, placer le flash de sorte qu'il réponde à une plage d'adresses autour de zéro. dites 0x0000 à 0x0FFF par exemple. puis mettre un bélier ailleurs.
Les concepteurs de puces savent comment le processeur démarre et ils y ont placé un stockage non volatile (flash / rom). Il appartient ensuite au logiciel d'écrire le code de démarrage pour qu'il corresponde au comportement bien connu de ce processeur. Vous devez placer l'adresse du vecteur de réinitialisation dans le vecteur de réinitialisation et votre code de démarrage à l'adresse que vous avez définie dans le vecteur de réinitialisation. La chaîne d'outils peut vous aider grandement ici. parfois, en particulier avec des ides pointer-cliquer ou d'autres bacs à sable, ils peuvent faire la plupart du travail pour vous tout ce que vous faites est d'appeler des API dans une langue de haut niveau (C).
Mais, cependant, le programme chargé dans le flash / rom doit correspondre au comportement de démarrage câblé du processeur. Avant la partie C de votre programme main () et si vous utilisez main comme point d'entrée, certaines choses doivent être faites. Le programmeur AC suppose que lorsque le déclarent une variable avec une valeur initiale, ils s'attendent à ce que cela fonctionne réellement. Eh bien, les variables, autres que const, sont en ram, mais si vous en avez une avec une valeur initiale, cette valeur initiale doit être en ram non volatile. Il s'agit donc du segment .data et le bootstrap C doit copier les données .data de Flash vers RAM (où est généralement déterminé pour vous par la chaîne d'outils). Les variables globales que vous déclarez sans valeur initiale sont supposées être nulles avant le démarrage de votre programme, bien que vous ne deviez vraiment pas le supposer et, heureusement, certains compilateurs commencent à avertir des variables non initialisées. Il s'agit du segment .bss et des zéros d'amorçage C qui, dans le ram, le contenu, les zéros, ne doivent pas être stockés dans la mémoire non volatile, mais l'adresse de départ et combien. Encore une fois, la chaîne d'outils vous aide beaucoup ici. Et enfin, le strict minimum est que vous devez configurer un pointeur de pile car les programmes C s'attendent à pouvoir avoir des variables locales et appeler d'autres fonctions. Alors peut-être que d'autres choses spécifiques aux puces sont effectuées, ou nous laissons le reste des choses spécifiques aux puces se produire en C. ne doit pas être stocké dans une mémoire non volatile, mais l'adresse de départ et combien. Encore une fois, la chaîne d'outils vous aide beaucoup ici. Et enfin, le strict minimum est que vous devez configurer un pointeur de pile car les programmes C s'attendent à pouvoir avoir des variables locales et appeler d'autres fonctions. Alors peut-être que d'autres choses spécifiques aux puces sont effectuées, ou nous laissons le reste des choses spécifiques aux puces se produire en C. ne doit pas être stocké dans une mémoire non volatile, mais l'adresse de départ et combien. Encore une fois, la chaîne d'outils vous aide beaucoup ici. Et enfin, le strict minimum est que vous devez configurer un pointeur de pile car les programmes C s'attendent à pouvoir avoir des variables locales et appeler d'autres fonctions. Ensuite, peut-être que d'autres choses spécifiques aux puces sont effectuées, ou nous laissons le reste des choses spécifiques aux puces se produire en C.
Les noyaux de la série cortex-m de arm feront une partie de cela pour vous, le pointeur de pile est dans la table vectorielle, il y a un vecteur de réinitialisation pour pointer sur le code à exécuter après la réinitialisation, de sorte que, à part ce que vous avez à faire pour générer la table vectorielle (que vous utilisez généralement asm de toute façon), vous pouvez utiliser du C pur sans asm. maintenant vous ne copiez pas vos .data ni votre .bss à zéro donc vous devez le faire vous-même si vous voulez essayer de vous passer de l'asm sur quelque chose basé sur cortex-m. La plus grande fonctionnalité n'est pas le vecteur de réinitialisation mais les vecteurs d'interruption où le matériel suit la convention d'appel C recommandée par les bras et préserve les registres pour vous, et utilise le retour correct pour ce vecteur, de sorte que vous n'ayez pas à enrouler le bon asm autour de chaque gestionnaire ( ou avoir des directives spécifiques à la chaîne d'outils pour votre cible afin que la chaîne d'outils l'enveloppe pour vous).
Les éléments spécifiques aux puces peuvent être par exemple, les microcontrôleurs sont souvent utilisés dans les systèmes à batterie, donc faible puissance, donc certains sortent de la réinitialisation avec la plupart des périphériques éteints, et vous devez allumer chacun de ces sous-systèmes pour pouvoir les utiliser . Uarts, gpios, etc. Souvent, une vitesse d'horloge faible est utilisée, directement à partir d'un cristal ou d'un oscillateur interne. Et la conception de votre système peut montrer que vous avez besoin d'une horloge plus rapide, vous devez donc l'initialiser. votre horloge peut être trop rapide pour le flash ou le bélier, vous devrez donc peut-être modifier les états d'attente avant de remonter l'horloge. Pourrait avoir besoin de configurer l'uart, l'USB ou d'autres interfaces. alors votre application peut faire son travail.
Un ordinateur de bureau, un ordinateur portable, un serveur et un microcontrôleur ne sont pas différents dans la façon dont ils démarrent / fonctionnent. Sauf qu'ils ne sont pas principalement sur une seule puce. Le programme de bios est souvent sur une puce / rom de puce distincte du processeur. Bien que récemment, les processeurs x86 tirent de plus en plus de puces de support dans le même paquet (contrôleurs pcie, etc.) mais vous avez toujours la plupart de vos RAM et rom hors puce, mais c'est toujours un système et cela fonctionne toujours exactement la même chose à un niveau élevé. Le processus de démarrage du processeur est bien connu, les concepteurs de cartes placent le flash / rom dans l'espace d'adressage où le processeur démarre. ce programme (qui fait partie du BIOS sur un PC x86) fait tout ce qui est mentionné ci-dessus, il démarre divers périphériques, il initialise dram, énumère les bus pcie, etc. Est souvent assez configurable par l'utilisateur en fonction des paramètres de bios ou de ce que nous appelions les paramètres de cmos, car à l'époque, c'était la technologie utilisée. Peu importe, il existe des paramètres utilisateur que vous pouvez modifier pour indiquer au code de démarrage du bios comment modifier ce qu'il fait.
différentes personnes utiliseront une terminologie différente. une puce démarre, c'est le premier code qui s'exécute. parfois appelé bootstrap. un chargeur de démarrage avec le mot chargeur signifie souvent que si vous ne faites rien pour interférer, c'est un bootstrap qui vous fait passer d'un démarrage générique à quelque chose de plus grand, votre application ou votre système d'exploitation. mais la partie chargeur implique que vous pouvez interrompre le processus de démarrage, puis charger d'autres programmes de test. si vous avez déjà utilisé uboot par exemple sur un système Linux embarqué, vous pouvez appuyer sur une touche et arrêter le démarrage normal, puis vous pouvez télécharger un noyau de test dans ram et le démarrer à la place de celui qui est sur flash, ou vous pouvez télécharger votre propres programmes, ou vous pouvez télécharger le nouveau noyau, puis faire en sorte que le chargeur de démarrage l'écrive sur flash afin que la prochaine fois que vous démarrez, il exécute les nouveaux éléments.
En ce qui concerne le processeur lui-même, le processeur de base, qui ne connaît pas la RAM du flash des périphériques. Il n'y a aucune notion de chargeur de démarrage, de système d'exploitation, d'application. Il s'agit simplement d'une séquence d'instructions qui sont introduites dans le processeur pour être exécutées. Ce sont des termes logiciels pour distinguer les différentes tâches de programmation les unes des autres. Concepts logiciels les uns des autres.
Certains microcontrôleurs ont un chargeur de démarrage séparé fourni par le fournisseur de puces dans un flash séparé ou une zone de flash séparée que vous ne pourrez peut-être pas modifier. Dans ce cas, il y a souvent une broche ou un ensemble de broches (je les appelle des sangles) que si vous les attachez haut ou bas avant la réinitialisation, vous dites à la logique et / ou au chargeur de démarrage quoi faire, par exemple une combinaison de sangles peut dites à la puce d'exécuter ce chargeur de démarrage et attendez sur l'uart que les données soient programmées dans le flash. Réglez les sangles dans l'autre sens et votre programme ne démarre pas le chargeur de démarrage des fournisseurs de puces, ce qui permet la programmation sur le terrain de la puce ou la récupération après le plantage de votre programme. Parfois, c'est juste une pure logique qui vous permet de programmer le flash. C'est assez courant de nos jours,
La raison pour laquelle la plupart des microcontrôleurs ont beaucoup plus de mémoire flash que de RAM est que le cas d'utilisation principal est d'exécuter le programme directement à partir de la mémoire flash et de n'avoir que suffisamment de mémoire RAM pour couvrir la pile et les variables. Bien que dans certains cas, vous pouvez exécuter des programmes à partir de ram que vous devez compiler correctement et stocker en flash puis copier avant d'appeler.
ÉDITER
flash.s
.cpu cortex-m0
.thumb
.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang
.thumb_func
reset:
bl notmain
b hang
.thumb_func
hang: b .
notmain.c
int notmain ( void )
{
unsigned int x=1;
unsigned int y;
y = x + 1;
return(0);
}
flash.ld
MEMORY
{
bob : ORIGIN = 0x00000000, LENGTH = 0x1000
ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > bob
.rodata : { *(.rodata*) } > bob
.bss : { *(.bss*) } > ted
.data : { *(.bss*) } > ted AT > bob
}
C'est donc un exemple pour un cortex-m0, les cortex-ms fonctionnent tous de la même manière dans cet exemple. La puce particulière, pour cet exemple, a un flash d'application à l'adresse 0x00000000 dans l'espace d'adresse de bras et un ram à 0x20000000.
La façon dont un cortex-m démarre est le mot de 32 bits à l'adresse 0x0000 est l'adresse pour initialiser le pointeur de pile. Je n'ai pas besoin de beaucoup de pile pour cet exemple, donc 0x20001000 suffira, évidemment il doit y avoir un bélier en dessous de cette adresse (la façon dont le bras pousse, est-il soustrait d'abord puis pousse donc si vous définissez 0x20001000 le premier élément de la pile est à l'adresse 0x2000FFFC vous n'avez pas besoin d'utiliser 0x2000FFFC). Le mot de 32 bits à l'adresse 0x0004 est l'adresse du gestionnaire de réinitialisation, essentiellement le premier code qui s'exécute après une réinitialisation. Ensuite, il y a plus de gestionnaires d'interruptions et d'événements spécifiques à ce noyau et à cette puce cortex m, peut-être jusqu'à 128 ou 256, si vous ne les utilisez pas, vous n'avez pas besoin de configurer la table pour eux, j'en ai ajouté quelques-uns pour la démonstration fins.
Je n'ai pas besoin de traiter avec .data ni .bss dans cet exemple parce que je sais déjà qu'il n'y a rien dans ces segments en regardant le code. S'il y en avait, je m'en occuperais et le ferai dans une seconde.
Donc, la pile est configurée, vérifier, .data pris en charge, vérifier, .bss, vérifier, donc le truc de bootstrap C est fait, peut se ramifier à la fonction d'entrée pour C. Parce que certains compilateurs ajouteront du courrier indésirable supplémentaire s'ils voient la fonction main () et sur le chemin de main, je n'utilise pas ce nom exact, j'ai utilisé notmain () ici comme point d'entrée C. Ainsi, le gestionnaire de réinitialisation appelle notmain () puis si / quand notmain () revient, il se bloque, ce qui n'est qu'une boucle infinie, éventuellement mal nommée.
Je crois fermement en la maîtrise des outils, beaucoup de gens ne le font pas, mais ce que vous trouverez, c'est que chaque développeur de métal nu fait son propre truc, en raison de la liberté presque complète, pas à distance aussi contraint que vous le feriez pour créer des applications ou des pages Web . Ils font à nouveau leur propre truc. Je préfère avoir mon propre code d'amorçage et script de l'éditeur de liens. D'autres s'appuient sur la chaîne d'outils ou jouent dans le bac à sable des vendeurs où la plupart du travail est effectué par quelqu'un d'autre (et si quelque chose se casse, vous êtes dans un monde de mal, et avec du métal nu, les choses se cassent souvent et de manière dramatique).
Donc, assembler, compiler et lier avec des outils gnu j'obtiens:
00000000 <_start>:
0: 20001000 andcs r1, r0, r0
4: 00000015 andeq r0, r0, r5, lsl r0
8: 0000001b andeq r0, r0, fp, lsl r0
c: 0000001b andeq r0, r0, fp, lsl r0
10: 0000001b andeq r0, r0, fp, lsl r0
00000014 <reset>:
14: f000 f802 bl 1c <notmain>
18: e7ff b.n 1a <hang>
0000001a <hang>:
1a: e7fe b.n 1a <hang>
0000001c <notmain>:
1c: 2000 movs r0, #0
1e: 4770 bx lr
Alors, comment le chargeur de démarrage sait-il où se trouvent les choses? Parce que le compilateur a fait le travail. Dans le premier cas, l'assembleur a généré le code pour flash.s et, ce faisant, sait où sont les étiquettes (les étiquettes ne sont que des adresses, tout comme les noms de fonction ou les noms de variable, etc.), donc je n'ai pas eu à compter les octets et à remplir le vecteur table manuellement, j'ai utilisé un nom d'étiquette et l'assembleur l'a fait pour moi. Maintenant vous demandez, si reset est l'adresse 0x14 pourquoi l'assembleur a mis 0x15 dans la table vectorielle. Eh bien, c'est un cortex-m et il démarre et ne fonctionne qu'en mode pouce. Avec ARM lorsque vous vous branchez sur une adresse si vous passez en mode pouce, le lsbit doit être défini, si le mode armé est réinitialisé. Vous avez donc toujours besoin de ce jeu de bits. Je connais les outils et en mettant .thumb_func avant une étiquette, si cette étiquette est utilisée telle qu'elle est dans la table vectorielle ou pour se ramifier ou autre. La chaîne d'outils sait définir le lsbit. Il a donc ici 0x14 | 1 = 0x15. De même pour accrocher. Maintenant, le désassembleur n'affiche pas 0x1D pour l'appel à notmain () mais ne vous inquiétez pas, les outils ont correctement construit l'instruction.
Maintenant que le code n'est pas principal, ces variables locales ne sont pas utilisées, elles sont du code mort. Le compilateur commente même ce fait en disant que y est défini mais non utilisé.
Notez l'espace d'adressage, ces choses commencent toutes à l'adresse 0x0000 et vont de là pour que la table vectorielle soit correctement placée, l'espace .text ou programme soit également correctement placé, comment j'ai obtenu flash.s devant le code de notmain.c par connaissant les outils, une erreur courante est de ne pas faire les choses correctement et de planter et de brûler dur. OMI, vous devez démonter pour vous assurer que les choses sont placées juste avant de démarrer la première fois, une fois que vous avez les choses au bon endroit, vous n'avez pas nécessairement à vérifier à chaque fois. Juste pour de nouveaux projets ou s'ils se bloquent.
Maintenant, ce qui surprend certains, c'est qu'il n'y a aucune raison de s'attendre à ce que deux compilateurs produisent la même sortie à partir de la même entrée. Ou même le même compilateur avec des paramètres différents. En utilisant clang, le compilateur llvm j'obtiens ces deux sorties avec et sans optimisation
llvm / clang optimisé
00000000 <_start>:
0: 20001000 andcs r1, r0, r0
4: 00000015 andeq r0, r0, r5, lsl r0
8: 0000001b andeq r0, r0, fp, lsl r0
c: 0000001b andeq r0, r0, fp, lsl r0
10: 0000001b andeq r0, r0, fp, lsl r0
00000014 <reset>:
14: f000 f802 bl 1c <notmain>
18: e7ff b.n 1a <hang>
0000001a <hang>:
1a: e7fe b.n 1a <hang>
0000001c <notmain>:
1c: 2000 movs r0, #0
1e: 4770 bx lr
non optimisé
00000000 <_start>:
0: 20001000 andcs r1, r0, r0
4: 00000015 andeq r0, r0, r5, lsl r0
8: 0000001b andeq r0, r0, fp, lsl r0
c: 0000001b andeq r0, r0, fp, lsl r0
10: 0000001b andeq r0, r0, fp, lsl r0
00000014 <reset>:
14: f000 f802 bl 1c <notmain>
18: e7ff b.n 1a <hang>
0000001a <hang>:
1a: e7fe b.n 1a <hang>
0000001c <notmain>:
1c: b082 sub sp, #8
1e: 2001 movs r0, #1
20: 9001 str r0, [sp, #4]
22: 2002 movs r0, #2
24: 9000 str r0, [sp, #0]
26: 2000 movs r0, #0
28: b002 add sp, #8
2a: 4770 bx lr
c'est donc un mensonge que le compilateur a optimisé l'ajout, mais il a alloué deux éléments sur la pile pour les variables, car ce sont des variables locales, elles sont en RAM mais sur la pile pas à des adresses fixes, verront avec des globaux que cela changements. Mais le compilateur s'est rendu compte qu'il pouvait calculer y au moment de la compilation et qu'il n'y avait aucune raison de le calculer au moment de l'exécution, il a donc simplement placé un 1 dans l'espace de pile alloué pour x et un 2 pour l'espace de pile alloué pour y. le compilateur "alloue" cet espace aux tables internes Je déclare pile plus 0 pour la variable y et pile plus 4 pour la variable x. le compilateur peut faire ce qu'il veut tant que le code qu'il implémente est conforme à la norme C ou aux expetations d'un programmeur C. Il n'y a aucune raison pour que le compilateur doive laisser x à la pile + 4 pendant la durée de la fonction,
Si j'ajoute un mannequin de fonction dans l'assembleur
.thumb_func
.globl dummy
dummy:
bx lr
puis appelez-le
void dummy ( unsigned int );
int notmain ( void )
{
unsigned int x=1;
unsigned int y;
y = x + 1;
dummy(y);
return(0);
}
la sortie change
00000000 <_start>:
0: 20001000 andcs r1, r0, r0
4: 00000015 andeq r0, r0, r5, lsl r0
8: 0000001b andeq r0, r0, fp, lsl r0
c: 0000001b andeq r0, r0, fp, lsl r0
10: 0000001b andeq r0, r0, fp, lsl r0
00000014 <reset>:
14: f000 f804 bl 20 <notmain>
18: e7ff b.n 1a <hang>
0000001a <hang>:
1a: e7fe b.n 1a <hang>
0000001c <dummy>:
1c: 4770 bx lr
...
00000020 <notmain>:
20: b510 push {r4, lr}
22: 2002 movs r0, #2
24: f7ff fffa bl 1c <dummy>
28: 2000 movs r0, #0
2a: bc10 pop {r4}
2c: bc02 pop {r1}
2e: 4708 bx r1
maintenant que nous avons des fonctions imbriquées, la fonction notmain doit conserver son adresse de retour, afin de pouvoir asservir l'adresse de retour de l'appel imbriqué. c'est parce que le bras utilise un registre pour les retours, s'il utilisait bien la pile comme disons un x86 ou quelques autres ... il utiliserait quand même la pile mais différemment. Maintenant, vous demandez pourquoi il a poussé R4? Eh bien, il n'y a pas longtemps, la convention d'appel a changé pour garder la pile alignée sur des limites de 64 bits (deux mots) au lieu de 32 bits, des limites d'un mot. Ils ont donc besoin de pousser quelque chose pour garder la pile alignée, donc le compilateur a choisi arbitrairement r4 pour une raison quelconque, peu importe pourquoi. Sauter dans r4 serait un bug, mais selon la convention d'appel pour cette cible, nous ne clobberions pas r4 sur un appel de fonction, nous pouvons clobber r0 à r3. r0 est la valeur de retour. On dirait qu'il fait peut-être une optimisation de queue,
Mais nous voyons que les mathématiques x et y sont optimisées pour qu'une valeur codée en dur de 2 soit transmise à la fonction factice (le factice a été spécifiquement codé dans un fichier séparé, dans ce cas asm, de sorte que le compilateur n'optimise pas complètement l'appel de fonction, si j'avais une fonction factice qui retournait simplement en C dans notmain.c, l'optimiseur aurait supprimé les appels de fonction x, y et factice car ils sont tous du code mort / inutile).
Notez également que parce que le code flash.s a grossi, notmain est ailleurs et que la chaîne d'outils a pris soin de corriger toutes les adresses pour nous, nous n'avons donc pas à le faire manuellement.
clang non optimisé pour référence
00000020 <notmain>:
20: b580 push {r7, lr}
22: af00 add r7, sp, #0
24: b082 sub sp, #8
26: 2001 movs r0, #1
28: 9001 str r0, [sp, #4]
2a: 2002 movs r0, #2
2c: 9000 str r0, [sp, #0]
2e: f7ff fff5 bl 1c <dummy>
32: 2000 movs r0, #0
34: b002 add sp, #8
36: bd80 pop {r7, pc}
clang optimisé
00000020 <notmain>:
20: b580 push {r7, lr}
22: af00 add r7, sp, #0
24: 2002 movs r0, #2
26: f7ff fff9 bl 1c <dummy>
2a: 2000 movs r0, #0
2c: bd80 pop {r7, pc}
cet auteur du compilateur a choisi d'utiliser r7 comme variable factice pour aligner la pile, il crée également un pointeur de cadre à l'aide de r7 même s'il n'a rien dans le cadre de la pile. fondamentalement, l'instruction aurait pu être optimisée. mais il a utilisé la pop pour ne pas renvoyer trois instructions, c'était probablement sur moi, je parie que je pourrais demander à gcc de le faire avec les bonnes options de ligne de commande (en spécifiant le processeur).
cela devrait surtout répondre au reste de vos questions
void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
y = x + 1;
dummy(y);
return(0);
}
J'ai des mondiaux maintenant. ils vont donc dans .data ou .bss s'ils ne sont pas optimisés.
avant de regarder la sortie finale, regardons l'objet itermediate
00000000 <notmain>:
0: b510 push {r4, lr}
2: 4b05 ldr r3, [pc, #20] ; (18 <notmain+0x18>)
4: 6818 ldr r0, [r3, #0]
6: 4b05 ldr r3, [pc, #20] ; (1c <notmain+0x1c>)
8: 3001 adds r0, #1
a: 6018 str r0, [r3, #0]
c: f7ff fffe bl 0 <dummy>
10: 2000 movs r0, #0
12: bc10 pop {r4}
14: bc02 pop {r1}
16: 4708 bx r1
...
Disassembly of section .data:
00000000 <x>:
0: 00000001 andeq r0, r0, r1
maintenant il manque des informations mais cela donne une idée de ce qui se passe, l'éditeur de liens est celui qui prend les objets et les relie avec les informations fournies (dans ce cas flash.ld) qui lui indiquent où .text et. données et ainsi de suite. le compilateur ne sait pas de telles choses, il ne peut se concentrer que sur le code qui lui est présenté, tout externe qu'il doit laisser un trou pour que l'éditeur de liens remplisse la connexion. Toutes les données doivent laisser un moyen de lier ces choses ensemble, donc les adresses pour tout sont basées sur zéro ici simplement parce que le compilateur et ce désassembleur ne le savent pas. il y a d'autres informations non montrées ici que l'éditeur de liens utilise pour placer des choses. le code ici est suffisamment indépendant de la position pour que l'éditeur de liens puisse faire son travail.
on voit alors au moins un démontage de la sortie liée
00000020 <notmain>:
20: b510 push {r4, lr}
22: 4b05 ldr r3, [pc, #20] ; (38 <notmain+0x18>)
24: 6818 ldr r0, [r3, #0]
26: 4b05 ldr r3, [pc, #20] ; (3c <notmain+0x1c>)
28: 3001 adds r0, #1
2a: 6018 str r0, [r3, #0]
2c: f7ff fff6 bl 1c <dummy>
30: 2000 movs r0, #0
32: bc10 pop {r4}
34: bc02 pop {r1}
36: 4708 bx r1
38: 20000004 andcs r0, r0, r4
3c: 20000000 andcs r0, r0, r0
Disassembly of section .bss:
20000000 <y>:
20000000: 00000000 andeq r0, r0, r0
Disassembly of section .data:
20000004 <x>:
20000004: 00000001 andeq r0, r0, r1
le compilateur a essentiellement demandé deux variables 32 bits dans ram. L'un est en .bss car je ne l'ai pas initialisé, il est donc supposé qu'il est initialisé à zéro. l'autre est .data parce que je l'ai initialisé lors de la déclaration.
Maintenant, comme ce sont des variables globales, on suppose que d'autres fonctions peuvent les modifier. le compilateur ne fait aucune hypothèse quant au moment où notmain peut être appelé, il ne peut donc pas optimiser avec ce qu'il peut voir, les mathématiques y = x + 1, donc il doit faire ce runtime. Il doit lire dans le ram les deux variables les ajouter et les sauvegarder.
Maintenant, clairement, ce code ne fonctionnera pas. Pourquoi? car mon bootstrap comme indiqué ici ne prépare pas le ram avant d'appeler notmain, donc quels que soient les déchets dans 0x20000000 et 0x20000004 lorsque la puce s'est réveillée, c'est ce qui sera utilisé pour y et x.
Je ne vais pas le montrer ici. vous pouvez lire mes randonnées encore plus longues sur .data et .bss et pourquoi je n'en ai jamais besoin dans mon code bare metal, mais si vous sentez que vous devez et voulez maîtriser les outils plutôt que d'espérer que quelqu'un d'autre l'a bien fait .. .
https://github.com/dwelch67/raspberrypi/tree/master/bssdata
les scripts de l'éditeur de liens et les bootstraps sont quelque peu spécifiques au compilateur, de sorte que tout ce que vous apprenez sur une version d'un compilateur pourrait être lancé sur la version suivante ou avec un autre compilateur, encore une autre raison pour laquelle je ne mets pas beaucoup d'efforts dans la préparation des .data et .bss juste pour être aussi paresseux:
unsigned int x=1;
Je préfère de beaucoup faire ça
unsigned int x;
...
x = 1;
et laissez le compilateur le mettre en .text pour moi. Parfois, il enregistre le flash de cette façon, parfois il brûle plus. Il est certainement beaucoup plus facile de programmer et de porter de la version de la chaîne d'outils ou d'un compilateur à un autre. Beaucoup plus fiable, moins sujet aux erreurs. Oui, n'est pas conforme à la norme C.
Et si on faisait ces globales statiques?
void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
y = x + 1;
dummy(y);
return(0);
}
bien
00000020 <notmain>:
20: b510 push {r4, lr}
22: 2002 movs r0, #2
24: f7ff fffa bl 1c <dummy>
28: 2000 movs r0, #0
2a: bc10 pop {r4}
2c: bc02 pop {r1}
2e: 4708 bx r1
de toute évidence, ces variables ne peuvent pas être modifiées par un autre code, de sorte que le compilateur peut désormais au moment de la compilation optimiser le code mort, comme il le faisait auparavant.
non optimisé
00000020 <notmain>:
20: b580 push {r7, lr}
22: af00 add r7, sp, #0
24: 4804 ldr r0, [pc, #16] ; (38 <notmain+0x18>)
26: 6800 ldr r0, [r0, #0]
28: 1c40 adds r0, r0, #1
2a: 4904 ldr r1, [pc, #16] ; (3c <notmain+0x1c>)
2c: 6008 str r0, [r1, #0]
2e: f7ff fff5 bl 1c <dummy>
32: 2000 movs r0, #0
34: bd80 pop {r7, pc}
36: 46c0 nop ; (mov r8, r8)
38: 20000004 andcs r0, r0, r4
3c: 20000000 andcs r0, r0, r0
ce compilateur qui utilisait la pile pour les locaux, utilise maintenant ram pour les globaux et ce code tel qu'il est écrit est cassé parce que je n'ai pas géré correctement .data ni .bss.
et une dernière chose que nous ne pouvons pas voir dans le démontage.
:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF
J'ai changé x pour être pré-init avec 0x12345678. Mon script de l'éditeur de liens (c'est pour gnu ld) a cette chose ted at bob. qui indique à l'éditeur de liens que je veux que l'emplacement final soit dans l'espace d'adressage ted, mais le stocke dans le binaire dans l'espace d'adressage ted et quelqu'un le déplacera pour vous. Et nous pouvons voir que cela s'est produit. c'est le format hexadécimal d'Intel. et nous pouvons voir le 0x12345678
:0400480078563412A0
se trouve dans l'espace d'adresse flash du binaire.
readelf montre aussi cela
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
EXIDX 0x010040 0x00000040 0x00000040 0x00008 0x00008 R 0x4
LOAD 0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
LOAD 0x020004 0x20000004 0x00000048 0x00004 0x00004 RW 0x10000
LOAD 0x030000 0x20000000 0x20000000 0x00000 0x00004 RW 0x10000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
la ligne LOAD où l'adresse virtuelle est 0x20000004 et la physique est 0x48
Cette réponse va se concentrer davantage sur le processus de démarrage. Tout d'abord, une correction - les écritures sur le flash sont effectuées après que le MCU (ou au moins une partie) a déjà démarré. Sur certains MCU (généralement les plus avancés), le CPU lui-même peut exploiter les ports série et écrire dans les registres flash. L'écriture et l'exécution du programme sont donc des processus différents. Je vais supposer que le programme a déjà été écrit en flash.
Voici le processus de démarrage de base. Je nommerai quelques variations courantes, mais surtout je reste aussi simple.
Réinitialiser: il existe deux types de base. Le premier est une réinitialisation à la mise sous tension, qui est générée en interne pendant que les tensions d'alimentation augmentent. Le second est une bascule de broche externe. Quoi qu'il en soit, la réinitialisation force toutes les bascules du MCU dans un état prédéterminé.
Initialisation matérielle supplémentaire: des cycles de temps et / ou d'horloge supplémentaires peuvent être nécessaires avant que le CPU ne démarre. Par exemple, dans les microcontrôleurs TI sur lesquels je travaille, une chaîne d'analyse de configuration interne est chargée.
Démarrage du processeur: le processeur récupère sa première instruction à partir d'une adresse spéciale appelée vecteur de réinitialisation. Cette adresse est déterminée lors de la conception du CPU. De là, c'est juste une exécution normale du programme.
Le CPU répète encore et encore trois étapes de base:
(C'est en fait plus compliqué que cela. Les processeurs sont généralement pipelinés , ce qui signifie qu'ils peuvent effectuer chacune des étapes ci-dessus sur différentes instructions en même temps. Chacune des étapes ci-dessus peut avoir plusieurs étapes de pipeline. Ensuite, il y a des pipelines parallèles, la prédiction de branche , et tous les éléments d'architecture informatique sophistiqués qui font que ces processeurs Intel prennent un milliard de transistors à concevoir.)
Vous vous demandez peut-être comment fonctionne la récupération. La CPU possède un bus composé de signaux d'adresse (sortie) et de données (entrée / sortie). Pour effectuer une extraction, la CPU définit ses lignes d'adresse à la valeur du compteur de programme, puis envoie une horloge sur le bus. L'adresse est décodée pour activer une mémoire. La mémoire reçoit l'horloge et l'adresse et place la valeur à cette adresse sur les lignes de données. Le CPU reçoit cette valeur. Les données lues et écrites sont similaires, sauf que l'adresse provient de l'instruction ou d'une valeur dans un registre à usage général, pas du PC.
Les CPU avec une architecture von Neumann ont un bus unique qui est utilisé pour les instructions et les données. Les CPU avec une architecture Harvard ont un bus pour les instructions et un pour les données. Dans les vrais MCU, ces deux bus peuvent être connectés aux mêmes mémoires, c'est donc souvent (mais pas toujours) quelque chose dont vous n'avez pas à vous soucier.
Retour au processus de démarrage. Après la réinitialisation, le PC est chargé avec une valeur de départ appelée le vecteur de réinitialisation. Cela peut être intégré au matériel ou (dans les processeurs ARM Cortex-M), il peut être lu automatiquement dans la mémoire. Le CPU récupère l'instruction du vecteur de réinitialisation et commence à parcourir les étapes ci-dessus. À ce stade, le CPU s'exécute normalement.
Chargeur de démarrage: il y a souvent une configuration de bas niveau qui doit être effectuée pour rendre le reste du MCU opérationnel. Cela peut inclure des choses comme l'effacement des RAM et le chargement des paramètres de finition de fabrication pour les composants analogiques. Il peut également y avoir une option pour charger le code à partir d'une source externe telle qu'un port série ou une mémoire externe. Le MCU peut inclure une ROM de démarrage qui contient un petit programme pour faire ces choses. Dans ce cas, le vecteur de réinitialisation du processeur pointe vers l'espace d'adressage de la ROM de démarrage. Il s'agit essentiellement d'un code normal, il est simplement fourni par le fabricant afin que vous n'ayez pas à l'écrire vous-même. :-) Dans un PC, le BIOS est l'équivalent de la ROM de démarrage.
Configuration de l'environnement C: C s'attend à avoir une pile (zone RAM pour stocker l'état pendant les appels de fonction) et des emplacements de mémoire initialisés pour les variables globales. Ce sont les sections .stack, .data et .bss dont Dwelch parle. Les valeurs globales initialisées ont leurs valeurs d'initialisation copiées du flash vers la RAM à cette étape. Les variables globales non initialisées ont des adresses RAM qui sont proches les unes des autres, de sorte que l'ensemble du bloc de mémoire peut être initialisé à zéro très facilement. La pile n'a pas besoin d'être initialisée (bien qu'elle puisse l'être) - tout ce que vous avez vraiment à faire est de définir le registre du pointeur de pile du CPU afin qu'il pointe vers une région affectée dans la RAM.
Fonction principale : Une fois l'environnement C configuré, le chargeur C appelle la fonction main (). C'est là que votre code d'application commence normalement. Si vous le souhaitez, vous pouvez laisser de côté la bibliothèque standard, ignorer la configuration de l'environnement C et écrire votre propre code pour appeler main (). Certains MCU peuvent vous permettre d'écrire votre propre chargeur de démarrage, puis vous pouvez faire vous-même toute la configuration de bas niveau.
Divers: De nombreux MCU vous permettront d'exécuter du code hors de la RAM pour de meilleures performances. Ceci est généralement installé dans la configuration de l'éditeur de liens. L'éditeur de liens attribue deux adresses à chaque fonction - une adresse de chargement , qui est l'endroit où le code est d'abord stocké (généralement flash), et une adresse d'exécution , qui est l'adresse chargée dans le PC pour exécuter la fonction (flash ou RAM). Pour exécuter du code à partir de la RAM, vous écrivez du code pour que le CPU copie le code de fonction de son adresse de chargement en flash vers son adresse d'exécution en RAM, puis appelez la fonction à l'adresse d'exécution. L'éditeur de liens peut définir des variables globales pour vous y aider. Mais l'exécution de code hors de la RAM est facultative dans les MCU. Vous ne le feriez normalement que si vous avez vraiment besoin de hautes performances ou si vous souhaitez réécrire le flash.
la source
Votre résumé est approximativement correct pour l' architecture de Von Neumann . Le code initial est généralement chargé dans la RAM via un chargeur de démarrage, mais pas (généralement) un chargeur de démarrage logiciel auquel le terme se réfère généralement. Il s'agit normalement d'un comportement «intégré dans le silicium». L'exécution de code dans cette architecture implique souvent une mise en cache prédictive des instructions de la ROM de telle sorte que le processeur maximise son temps d'exécution du code et n'attende pas que le code soit chargé dans la RAM. J'ai lu quelque part que le MSP430 est un exemple de cette architecture.
Dans un périphérique Harvard Architecture , les instructions sont exécutées directement à partir de la ROM tandis que la mémoire de données (RAM) est accessible via un bus séparé. Dans cette architecture, le code commence simplement à s'exécuter à partir du vecteur de réinitialisation. Le PIC24 et le dsPIC33 sont des exemples de cette architecture.
Quant au retournement réel des bits qui déclenche ces processus, cela peut varier d'un appareil à l'autre et peut impliquer des débogueurs, JTAG, des méthodes propriétaires, etc.
la source