Sprechen Sie über den SOC-Startvorgang (4) und den ATF BL31-Startvorgang

https://zhuanlan.zhihu.com/p/520052961

Dieser Artikel basiert auf den folgenden Hardware- und Softwareannahmen:

Architektur: AARCH64

Software: ATF V2.5

1 BL31-Startvorgang

Im Gegensatz zu bl1 und bl2 enthält bl31 zwei Funktionen. Es wird als Teil des Startvorgangs beim Start verwendet, um eine Software- und Hardware-Initialisierung durchzuführen und bl32- und bl33-Images zu starten. Nach Abschluss des Systemstarts verbleibt es weiterhin im System und verarbeitet SMC-Ausnahmen von anderen Ausnahmestufen sowie andere Interrupts, die zur Verarbeitung an EL3 weitergeleitet werden müssen. Daher umfasst der Startvorgang von bl31 hauptsächlich die folgenden Aufgaben:

(1) CPU-Initialisierung

(2) Initialisierung der C-Laufzeitumgebung

(3) Grundlegende Hardware-Initialisierung, z. B. GIC, serielle Schnittstelle, Timer usw.

(4) Erstellung von Seitentabellen und Cache-Aktivierung

(5) Vorbereitung zum Starten des Post-Level-Images und zum Springen zum neuen Image

(6) Wenn bl31 EL3-Interrupt unterstützt, müssen Sie das Interrupt-Verarbeitungsframework initialisieren

(7) SMC-Verarbeitung verschiedener sicherer Zustände zur Laufzeit und Initialisierung des Ausnahmeebenen-Umschaltkontexts

(8) Registrierung des Laufzeitdienstes zur Verarbeitung von SMC-Befehlen

Weniger Klatsch, das übliche Bild oben:

Fügen Sie hier eine Bildbeschreibung ein

2 bl31 Grundinitialisierung

2.1 Parameterspeicherung

mov	x20, x0
mov	x21, x1
mov	x22, x2
mov	x23, x3

Speichern Sie wie bei bl2 die von bl2 übergebenen Parameter aus dem Anruferregister im Angerufenenregister

2.2 el3_entrypoint_common-Funktion

Diese Funktion wurde in bl1 ausführlich eingeführt, die Aufrufmethode von bl31 unterscheidet sich jedoch immer noch von der von bl1. Schauen wir uns den Aufruf in bl31 an:

#if !RESET_TO_BL31
	el3_entrypoint_common					\
		_init_sctlr=0					\
		_warm_boot_mailbox=0				\
		_secondary_cold_boot=0				\
		_init_memory=0					\
		_init_c_runtime=1				\
		_exception_vectors=runtime_exceptions		\
		_pie_fixup_size=BL31_LIMIT - BL31_BASE
#else
		el3_entrypoint_common					\
		_init_sctlr=1					\
		_warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS	\
		_secondary_cold_boot=!COLD_BOOT_SINGLE_CPU	\
		_init_memory=1					\
		_init_c_runtime=1				\
		_exception_vectors=runtime_exceptions		\
		_pie_fixup_size=BL31_LIMIT - BL31_BASE
	mov	x20, 0
	mov	x21, 0
	mov	x22, 0
	mov	x23, 0
#endif

Wie aus dem obigen Code ersichtlich ist, verfügt diese Funktion je nachdem, ob RESET_TO_BL31 gesetzt ist oder nicht, über zwei unterschiedliche Sätze von Aufrufparametern. Dies liegt daran, dass atf zwei Boot-Methoden unterstützt:

(1) Der Start beginnt mit bl1, der Standard-Startmethode von ATF. Da bl1 zu diesem Zeitpunkt die Funktion el3_entrypoint_common ausgeführt hat, ist die Grundkonfiguration des Systems eingerichtet. Daher wurde der Prozess zum Festlegen des SCTLR-Registers, der Hot-Start-Sprungverarbeitung, der sekundären CPU-Verarbeitung und des Speicherinitialisierungsprozesses in bl1 abgeschlossen und kann in bl31 übersprungen werden

(2) Die Grundlage für die Unterstützung ab bl31 ist, dass armv8 das dynamische Festlegen der Neustartadresse der CPU unterstützt. Die armv8-Architektur stellt das RVBAR-Register (Reset Vector Base Address Register) zum Festlegen der Startposition der CPU während des Zurücksetzens bereit. Es gibt drei Register: RVBAR_EL1, RVBAR_EL2 und RVBAR_EL3. Welches verwendet werden soll, hängt von der höchsten vom System implementierten Ausnahmestufe ab. Wir wissen, dass der Neustart von armv8 immer von der höchsten Ausnahmestufe aus ausgeführt wird, daher müssen wir nur das RVBAR-Register mit der höchsten Ausnahmestufe festlegen. Da bl31 unter el3 ausgeführt wird, können wir die Unterstützung ab bl31 durch Festlegen der Adresse im RVBAR_EL3-Register realisieren.

Wenn der Start mit bl31 beginnt, muss el3_entrypoint_common den Systemstatus von Anfang an festlegen, da es sich um das Startbild der ersten Ebene handelt, sodass das SCTLR-Register, die Startsprungverarbeitung, die sekundäre CPU-Verarbeitung und der Speicherinitialisierungsprozess in dieser Funktion erforderlich sind ausgeführt werden.

Obwohl el3_entrypoint_common viel Arbeit leisten muss, überspringt diese Methode direkt den zweistufigen Startprozess von bl1 und bl2. Im Vergleich zur ersten Methode ist die Startgeschwindigkeit schneller, was auch der größte Vorteil ist.

Die letzte Möglichkeit, den Wert der Parameterspeicherregister x20 – x23 zu löschen, ist ebenfalls sehr verständlich, da bl31 zu diesem Zeitpunkt das Bild der ersten Ebene ist, das gestartet wird, und natürlich keine Parameter vom Bild der vorherigen Ebene übergeben werden Zeit, das Löschen dieser Werte kann durchgeführt werden Vermeiden Sie Probleme beim späteren Parsen von Parametern.

3 bl31 Parametereinstellung

3.1 bl31_early_platform_setup2

Diese Funktion initialisiert zunächst die qemu-Konsole, analysiert dann die von bl2 übergebenen Parameter der verknüpften Bildbeschreibung und speichert die analysierten bl32- und bl33-Bilder ep_info in der globalen Variablen. Sein Hauptprozess ist wie folgt:

qemu_console_init();1bl_params_t *params_from_bl2 = (bl_params_t *)arg0;2
	bl_params_node_t *bl_params = params_from_bl2->head;3while (bl_params) {
    
    4if (bl_params->image_id == BL32_IMAGE_ID) {
    
    
			bl32_image_ep_info = *bl_params->ep_info;5}

		if (bl_params->image_id == BL33_IMAGE_ID){
    
    
			bl33_image_ep_info = *bl_params->ep_info;6}

		bl_params = bl_params->next_params_info;
	}
	if (!bl33_image_ep_info.pc)7panic();

(1) Konsoleninitialisierung

(2) Erhalten Sie den von arg0 übergebenen Bildbeschreibungsparameterzeiger

(3) Erhalten Sie den Kopfknoten der Spiegelliste

(4) Durchlaufen der Spiegelliste

(5) Wenn die verknüpfte Liste einen bl32-Bilddeskriptor enthält, speichern Sie dessen ep_info in einer globalen Variablen

(6) Viele der verknüpften Listen enthalten bl33-Bilddeskriptoren und speichern ihre ep_info auch in globalen Variablen

(7) Überprüfen Sie die Eintragsadresse des bl33-Images

3.2 bl31_plat_arch_setup

Diese Funktion wird verwendet, um eine Seitentabelle für bl31-bezogenen Speicher zu erstellen und MMU und DCache zu aktivieren. Der Code lautet wie folgt:

void bl31_plat_arch_setup(void)

{
    
    
	qemu_configure_mmu_el3(BL31_BASE, (BL31_END - BL31_BASE),

	BL_CODE_BASE, BL_CODE_END,

	BL_RO_DATA_BASE, BL_RO_DATA_END,

	BL_COHERENT_RAM_BASE, BL_COHERENT_RAM_END);
}

4 bl31 Hauptverarbeitungsfunktion

4.1 bl31_platform_setup

Diese Funktion ist plattformabhängig und die Implementierung der qemu-Plattform ist wie folgt:

void bl31_platform_setup(void)
{
    
    
	plat_qemu_gic_init();1qemu_gpio_init();2}

(1) GIC initialisieren, einschließlich der Initialisierung des GIC-Verteilers, des Weiterverteilers, der CPU-Schnittstelle usw. Den detaillierten Prozess der BL31-GIC- und Interrupt-Verarbeitung finden Sie im folgenden Blogbeitrag:
https://blog.csdn.net/lgjjeff/article/details/122402214?spm=1001.2014.3001.5502

(2) Initialisieren Sie das GPIO der QEMU-Plattform, dh legen Sie die GPIO-Basisadresse und betriebsbezogene Rückruffunktionen dafür fest

4.2 EHF-Initialisierung

ehf wird verwendet, um Funktionen im Zusammenhang mit der EL3-Interrupt-Verarbeitung zu initialisieren. Interrupts in gicv3 sind in drei Gruppen unterteilt: Gruppe0, sichere Gruppe1 und nicht sichere Gruppe 1, die je nach Konfiguration der IRQ- und Fiq-Bits von scr_el3 an verschiedene Ausnahmestufen weitergeleitet werden können. Ehf wird zur Verarbeitung von Group0-Interrupts verwendet. Dieser Interrupt wird immer in Form von fiq ausgelöst. Durch Festlegen von scr_el3 zur Weiterleitung an el3 zur Verarbeitung kann dieser Interrupttyp in bl31 verarbeitet werden. Informationen zum Prinzip des Interrupt-Routings finden Sie unter:
https://blog.csdn.net/lgjjeff/article/details/110729661?spm=1001.2014.3001.5502

Der EHF-Initialisierungsprozess besteht hauptsächlich darin, den Routing-Modus der Gruppe 0 festzulegen und eine allgemeine Interrupt-Verarbeitungsfunktion dafür festzulegen. Sein Hauptprozess ist wie folgt:

void __init ehf_init(void)
{
    
    
	unsigned int flags = 0;
	int ret __unused;
	set_interrupt_rm_flag(flags, NON_SECURE);
	set_interrupt_rm_flag(flags, SECURE);1

	ret = register_interrupt_type_handler(INTR_TYPE_EL3,
			ehf_el3_interrupt_handler, flags);2assert(ret == 0);
}

(1) Berechnen Sie das Flag für das Interrupt-Routing

(2) Stellen Sie den Interrupt-Routing-Modus des Interrupts vom Typ EL3 (Gruppe 0) und die allgemeine Interrupt-Verarbeitungsfunktion von bl31 ein

Die bl31-Interrupt-Verarbeitungsfunktion ehf_el3_interrupt_handler wird vom Verarbeitungsablauf der Ausnahmevektortabelle aufgerufen und ruft weiterhin die eigentliche Verarbeitungsfunktion auf, die jeder Prioritätsstufe entsprechend der Interrupt-Prioritätsstufe entspricht. Der Registrierungsprozess der Verarbeitungsfunktion entsprechend der Interrupt-Priorität ist in die folgenden zwei Schritte unterteilt. Das Folgende ist ein Beispiel für den Interrupt-Registrierungsprozess:

ehf_pri_desc_t plat_exceptions[] = {
    
    
#if RAS_EXTENSION
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_RAS_PRI),
#endif
#if SDEI_SUPPORT
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SDEI_CRITICAL_PRI),
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SDEI_NORMAL_PRI),
#endif
#if SPM_MM
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SP_PRI),
#endif
#ifdef PLAT_EHF_DESC
	PLAT_EHF_DESC,
#endif
};

EHF_REGISTER_PRIORITIES(plat_exceptions, ARRAY_SIZE(plat_exceptions), PLAT_PRI_BITS);

Im obigen Beispiel werden Interrupts wie RAS und SDEI registriert und ihnen unterschiedliche Prioritäten zugewiesen. Zu diesem Zeitpunkt ist jedoch nur ein Platz für die Interrupt-Verarbeitungsfunktion belegt und nicht tatsächlich definiert. Sie werden tatsächlich über ehf_register_priority_handler im Treiber registriert. Für Sdei läuft der Registrierungsprozess beispielsweise wie folgt ab:

void sdei_init(void)
{
    
    
	ehf_register_priority_handler(PLAT_SDEI_CRITICAL_PRI,
			sdei_intr_handler);
	ehf_register_priority_handler(PLAT_SDEI_NORMAL_PRI,
			sdei_intr_handler);
}

Wenn ehf_register_priority_handler registriert ist, kann bl31 theoretisch el3-Interrupts empfangen und verarbeiten. Tatsächlich sind jedoch bei der Ausführung von bl31 alle IRQ- und Fiq-Interrupt-Masken von PSTATE maskiert, dh der EL3-Interrupt kann nur ausgelöst und verarbeitet werden, wenn die CPU unterhalb der EL3-Ausnahmestufe läuft.

4.3 Initialisierung des Laufzeitdienstes

Wir haben bereits erwähnt, dass bl31 nach Abschluss der Systeminitialisierung im System verbleiben und SMC-Ausnahmen von niedrigen Ausnahmestufen verarbeiten muss. Sein Ausnahmebehandlungsprozess wird als Laufzeitdienst bezeichnet. Arm definiert eine Reihe von Spezifikationen für ihre Nutzungsszenarien, die zur Bewältigung verschiedener Arten von Aufgaben verwendet werden, wie z. B. die CPU-Energieverwaltungsspezifikation PSCI, die Software-Event-Proxy-Spezifikation SDEI für die Proxy-Verteilung in nicht-sicheren Welten zur Verarbeitung von Interrupts und für Aufrufe im Zusammenhang mit Vertrauenssystemen SPD usw. Bevor diese Dienste verwendet werden, müssen ihre Dienstverarbeitungsfunktionen natürlich in bl31 registriert werden, und zu diesem Zweck wird der Laufzeitdienstinitialisierungsprozess verwendet.

Bevor wir den Initialisierungsprozess des Laufzeitdienstes analysieren, werfen wir einen Blick auf seine Registrierungsmethode. Das Folgende ist die Definition seiner Registrierungsschnittstelle DECLARE_RT_SVC:

#define DECLARE_RT_SVC(_name, _start, _end, _type, _setup, _smch)	\
	static const rt_svc_desc_t __svc_desc_ ## _name			\                 (1
		__section("rt_svc_descs") __used = {
    
    			\                 (2.start_oen = (_start),				\
			.end_oen = (_end),				\
			.call_type = (_type),				\
			.name = #_name,					\
			.init = (_setup),				\
			.handle = (_smch)				\
		}

Die Schnittstelle definiert eine Struktur __svc_desc_ ## _name und fügt sie in ein spezielles Segment rt_svc_descs ein. Die Definition dieses Abschnitts befindet sich in der Link-Skript-Header-Datei include/common/bl_common.ld.h, die wie folgt definiert ist:

#define RT_SVC_DESCS                                    \
        . = ALIGN(STRUCT_ALIGN);                        \
        __RT_SVC_DESCS_START__ = .;                     \
        KEEP(*(rt_svc_descs))                           \
        __RT_SVC_DESCS_END__ = .;

Das heißt, diese registrierten Laufzeitdienststrukturen werden im Segment rt_svc_descs gespeichert, beginnend mit __RT_SVC_DESCS_START__ und endend mit __RT_SVC_DESCS_END__, und ihre Daten können als folgende Struktur ausgedrückt werden:

Fügen Sie hier eine Bildbeschreibung ein
Wenn Sie diese Strukturzeiger benötigen, müssen Sie daher nur diese Adresse durchlaufen. Dies ist der Fall für den Ablauf der Laufzeitdienst-Initialisierungsfunktion runtime_svc_init, der wie folgt definiert ist:

void __init runtime_svc_init(void)
{
    
    
	rt_svc_descs = (rt_svc_desc_t *) RT_SVC_DESCS_START;1for (index = 0U; index < RT_SVC_DECS_NUM; index++) {
    
    2rt_svc_desc_t *service = &rt_svc_descs[index];

			rc = validate_rt_svc_desc(service);3if (rc != 0) {
    
    
			ERROR("Invalid runtime service descriptor %p\n",
				(void *) service);
			panic();
		}

		if (service->init != NULL) {
    
                
			rc = service->init();4if (rc != 0) {
    
    
				ERROR("Error initializing runtime service %s\n",
						service->name);
				continue;
			}
		}
	}
}

(1) Rufen Sie die Startadresse des rt_svc_descs-Segments RT_SVC_DESCS_START ab

(2) Durchlaufen Sie die entsprechenden Laufzeitdienste aller registrierten rt_svc_desc_t-Strukturen in diesem Segment

(3) Überprüfen Sie die Gültigkeit des Laufzeitdienstes

(4) Rufen Sie den dem Dienst entsprechenden Initialisierungsrückruf auf, der über den Parameter _setup im Registrierungsmakro DECLARE_RT_SVC übergeben wird

4.4 Starten Sie bl32

Bl32 wird hauptsächlich zum Ausführen von Trust OS verwendet, das hauptsächlich zum Schutz sensibler Benutzerdaten (wie Passwörter, Fingerabdrücke, Gesichter usw.) und verwandter Funktionsmodule wie Verschlüsselungs- und Entschlüsselungsalgorithmen sowie zum sicheren Laden und Ausführen von Tas verwendet wird Lagerung usw. Die Trust-OS-Implementierungen verschiedener Hersteller sind unterschiedlich, die Grundideen sind jedoch ähnlich. Wenn es in der folgenden Analyse um bestimmte Trust-OS geht, nehmen wir das Open-Source-Framework optee als Beispiel.

Der BL32-Ausführungsprozess im Startvorgang ist wie folgt:

if (bl32_init != NULL) {
    
    
		INFO("BL31: Initializing BL32\n");

		int32_t rc = (*bl32_init)();

		if (rc == 0)
			WARN("BL31: BL32 initialization failed\n");
	}

Es beurteilt zunächst, ob bl32_init registriert wurde, und führt in diesem Fall den tatsächlichen BL32-Operationsprozess durch Aufrufen dieser Funktion aus. Schauen wir uns zunächst den Registrierungsprozess bl32_init (services/spd/opteed) unter der Optee-Architektur an:

DECLARE_RT_SVC(
	opteed_fast,

	OEN_TOS_START,
	OEN_TOS_END,
	SMC_TYPE_FAST,
	opteed_setup,1
	opteed_smc_handler
);

static int32_t opteed_setup(void)
{
    
    
	bl31_register_bl32_init(&opteed_init)2return 0;
}

void bl31_register_bl32_init(int32_t (*func)(void))
{
    
    
	bl32_init = func;3}

(1) Legen Sie den Initialisierungsrückruf von optee opteed_setup über DECLARE_RT_SVC fest

(2) Registrieren Sie die Funktion opteed_init als Startfunktion von bl32

(3) Tatsächliche Rückrufregistrierung

Daher ist die bl32-Startfunktion von optee opteed_init, und ihr Prozess ähnelt unserer vorherigen Sprungmethode von bl1, die bl2 startet. Das Flussdiagramm lautet wie folgt:

Fügen Sie hier eine Bildbeschreibung ein

Zunächst werden die zuvor gespeicherten EP-Informationen des sicheren Bildes (dh die EP-Informationen von bl32) abgerufen und dann zum Initialisieren des Kontexts zum Umschalten der Ausnahmeebene verwendet und die Systemregister von Secure EL1, SPSR_EL3 und ELR_EL3 usw. festgelegt. Rufen Sie dann die Funktion opteed_enter_sp auf, um zu bl32 zu springen. Hier liegt ein Problem vor. Zusätzlich zum Starten von bl32 muss bl31 weiterhin bl33 starten. Daher muss nach dem Start von bl32 zu bl31 zurückgesprungen werden und der Startvorgang von bl33 weiterhin ausgeführt werden. Da bl32 im sicheren EL1 ausgeführt wird, kann sein synchroner Zugriff auf bl31 nur die SMC-Methode verwenden und muss daher zum ursprünglichen Haltepunkt im SMC-Verarbeitungsfluss springen. Das lr-Register der C-Sprache in Armv8 ist x30. Wenn wir also x30 und den laufenden Kontext vor dem Sprung speichern und diese Kontexte dann im SMC-Verarbeitungsfluss wiederherstellen, kann die Ausführung am Haltepunkt fortgesetzt werden. Das Folgende ist der Kontextspeicherprozess der Funktion opteed_enter_sp:

func opteed_enter_sp
	mov	x3, sp
	str	x3, [x0, #0]
	sub	sp, sp, #OPTEED_C_RT_CTX_SIZE

	stp	x19, x20, [sp, #OPTEED_C_RT_CTX_X19]
	stp	x21, x22, [sp, #OPTEED_C_RT_CTX_X21]
	stp	x23, x24, [sp, #OPTEED_C_RT_CTX_X23]
	stp	x25, x26, [sp, #OPTEED_C_RT_CTX_X25]
	stp	x27, x28, [sp, #OPTEED_C_RT_CTX_X27]
	stp	x29, x30, [sp, #OPTEED_C_RT_CTX_X29]

	b	el3_exit
endfunc opteed_enter_sp

In dieser Funktion wird der Kontext in der globalen Variablen opteed_sp_context gespeichert, und der Prozess der Rückkehr zu smc nach Abschluss der Optee-Initialisierung ist wie folgt (services/spd/opteed/opteed_main.c):

uintptr_t opteed_smc_handler()
{
    
    
optee_context_t *optee_ctx = &opteed_sp_context[linear_id];
	switch (smc_fid) {
    
    
	case TEESMC_OPTEED_RETURN_ENTRY_DONE:1assert(optee_vector_table == NULL);
		optee_vector_table = (optee_vectors_t *) x1;
		opteed_synchronous_sp_exit(optee_ctx, x1);2break;
	}
}

(1) Zeigt an, dass dieser SMC-Aufruf zurückgegeben wird, nachdem der BL32-Start abgeschlossen ist

(2) Rufen Sie diese Funktion auf, um den vor der Eingabe von bl32 gespeicherten Kontext wiederherzustellen, und kehren Sie zum Haltepunkt zurück, um die Ausführung fortzusetzen. Die Funktion ist wie folgt definiert:

func opteed_exit_sp
	mov	sp, x0                                                                  (1

	ldp	x19, x20, [x0, #(OPTEED_C_RT_CTX_X19 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x21, x22, [x0, #(OPTEED_C_RT_CTX_X21 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x23, x24, [x0, #(OPTEED_C_RT_CTX_X23 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x25, x26, [x0, #(OPTEED_C_RT_CTX_X25 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x27, x28, [x0, #(OPTEED_C_RT_CTX_X27 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x29, x30, [x0, #(OPTEED_C_RT_CTX_X29 - OPTEED_C_RT_CTX_SIZE)]2

	mov	x0, x1
	ret                                                                              (3
endfunc opteed_exit_sp

(1) Stellen Sie den im Kontext gespeicherten Stapel wieder her, bevor Sie bl32 eingeben

(2) Stellen Sie das vor der Eingabe von bl32 gespeicherte Angerufene-Register wieder her

(3) Kehren Sie zum Haltepunkt zurück, um die Ausführung fortzusetzen, gehen Sie herum und gehen Sie herum. Schließlich sind wir zur Funktion bl31_main zurückgekehrt

Verwenden Sie abschließend ein Diagramm, um den gesamten Prozess oben zu beschreiben:

Fügen Sie hier eine Bildbeschreibung ein
4.4 Starten Sie bl33

Der bl33-Startvorgang ähnelt auf allen Ebenen dem vorherigen Image-Startvorgang. Außerdem werden der Kontext, die Eintragsadresse und die Parameter von bl33 gemäß ep_info festgelegt und dann zur Ausführung zum Eintrag gesprungen. Wenn Sie interessiert sind, können Sie es anhand des Codes selbst analysieren, daher werde ich es hier nicht wiederholen. Nun, der ATF-Startvorgang ist endlich abgeschlossen, und dann springen wir in die Welt von bl33 (uboot). Alle Vorbereitungen sind für den schönen Moment, in dem uboot den Kernel startet, viel Spaß!

Je suppose que tu aimes

Origine blog.csdn.net/qq_41483419/article/details/130975803
conseillé
Classement