Changeset 5859 for trunk/src/kernel32/winimagepeldr.cpp
- Timestamp:
- Jun 1, 2001, 10:10:48 AM (24 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/kernel32/winimagepeldr.cpp
r5850 r5859 1 /* $Id: winimagepeldr.cpp,v 1.80 2001-06-01 08:10:48 sandervl Exp $ */ 2 1 3 /* 2 4 * Win32 PE loader Image base class … … 53 55 #include <wprocess.h> 54 56 55 56 57 //Define COMMIT_ALL to let the pe loader commit all sections of the image 57 58 //This is very useful during debugging as you'll get lots of exceptions … … 109 110 nrsections(0), imageSize(0), dwFlags(0), section(NULL), 110 111 imageVirtBase(-1), realBaseAddress(0), imageVirtEnd(0), 111 nrNameExports(0), nrOrdExports(0), 112 nameexports(NULL), ordexports(NULL), 113 curnameexport(NULL), curordexport(NULL), 114 memmap(NULL), pFixups(NULL), dwFixupSize(0) 112 nrNameExports(0), nrOrdExports(0), nameexports(NULL), ordexports(NULL), 113 memmap(NULL), pFixups(NULL), dwFixupSize(0), curnameexport(NULL), curordexport(NULL) 115 114 { 116 115 HFILE dllfile; … … 218 217 //Allocate memory to hold the entire image 219 218 if(allocSections(reservedMem) == FALSE) { 220 dprintf((LOG, "Failed to allocate image memory for %s at %x, rc %d", szFileName, oh.ImageBase, errorState)); 219 dprintf((LOG, "Failed to allocate image memory for %s at %x, rc %d", szFileName, oh.ImageBase, errorState));; 221 220 goto failure; 222 221 } … … 651 650 } 652 651 } 653 654 652 return(TRUE); 655 653 … … 666 664 return FALSE; 667 665 } 666 //****************************************************************************** 667 //****************************************************************************** 668 #define DOSREAD_IDEAL_SIZE 61440 669 static inline APIRET _Optlink fastDosRead(HFILE hFile, 670 PVOID pAddress, 671 ULONG ulSize, 672 PULONG pulBytesRead) 673 { 674 /* we better break the DosRead into multiple calls */ 675 PBYTE p = (PBYTE)pAddress; 676 ULONG ulReadBytes; 677 APIRET rc; 678 679 *pulBytesRead = ulSize; 680 681 do 682 { 683 rc = DosRead(hFile, 684 p, 685 min(DOSREAD_IDEAL_SIZE, ulSize), 686 &ulReadBytes); 687 if (rc != NO_ERROR) 688 { 689 /* in case of errors bail out */ 690 *pulBytesRead = 0; 691 return rc; 692 } 693 694 ulSize -= ulReadBytes; 695 p += ulReadBytes; 696 } 697 while (ulSize > 0); 698 699 return NO_ERROR; 700 } 701 668 702 //****************************************************************************** 669 703 // commitPage: … … 690 724 // 691 725 //****************************************************************************** 692 693 #define DOSREAD_IDEAL_SIZE 61440694 static inline APIRET _Optlink fastDosRead(HFILE hFile,695 PVOID pAddress,696 ULONG ulSize,697 PULONG pulBytesRead)698 {699 /* we better break the DosRead into multiple calls */700 PBYTE p = (PBYTE)pAddress;701 ULONG ulReadBytes;702 APIRET rc;703 704 *pulBytesRead = ulSize;705 706 do707 {708 rc = DosRead(hFile,709 p,710 min(DOSREAD_IDEAL_SIZE, ulSize),711 &ulReadBytes);712 if (rc != NO_ERROR)713 {714 /* in case of errors bail out */715 *pulBytesRead = 0;716 return rc;717 }718 719 ulSize -= ulReadBytes;720 p += ulReadBytes;721 }722 while (ulSize > 0);723 724 return NO_ERROR;725 }726 727 728 726 BOOL Win32PeLdrImage::commitPage(ULONG virtAddress, BOOL fWriteAccess, int fPageCmd) 729 727 { … … 821 819 goto fail; 822 820 } 823 821 #if 1 824 822 // 2001-05-31 PH 825 823 // ensure DosRead() does not have to read more 826 824 // than 65535 bytes, otherwise split into two requests! 827 825 rc = fastDosRead(hFile, (PVOID)virtAddress, size, &ulRead); 826 #else 827 rc = DosRead(hFile, (PVOID)virtAddress, size, &ulRead); 828 #endif 828 829 if(rc) { 829 830 DosExitCritSec(); … … 1319 1320 BOOL Win32PeLdrImage::processExports(char *win32file) 1320 1321 { 1321 1322 1323 1324 1325 1326 1322 IMAGE_SECTION_HEADER sh; 1323 PIMAGE_EXPORT_DIRECTORY ped; 1324 ULONG *ptrNames, *ptrAddress; 1325 USHORT *ptrOrd; 1326 BOOL fForwarder; 1327 int i; 1327 1328 1328 1329 /* get section header and pointer to data directory for .edata section */ 1329 1330 if((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset 1330 (win32file, IMAGE_DIRECTORY_ENTRY_EXPORT)) != NULL && 1331 GetSectionHdrByImageDir(win32file, IMAGE_DIRECTORY_ENTRY_EXPORT, &sh) ) 1332 { 1333 1334 dprintf((LOG, "Exported Functions: " )); 1331 (win32file, IMAGE_DIRECTORY_ENTRY_EXPORT)) != NULL && 1332 GetSectionHdrByImageDir(win32file, IMAGE_DIRECTORY_ENTRY_EXPORT, &sh) ) { 1333 1334 dprintf((LOG, "Exported Functions: " )); 1335 1335 ptrOrd = (USHORT *)((ULONG)ped->AddressOfNameOrdinals + 1336 1336 (ULONG)win32file); 1337 1337 ptrNames = (ULONG *)((ULONG)ped->AddressOfNames + 1338 (ULONG)win32file);1338 (ULONG)win32file); 1339 1339 ptrAddress = (ULONG *)((ULONG)ped->AddressOfFunctions + 1340 (ULONG)win32file);1340 (ULONG)win32file); 1341 1341 nrOrdExports = ped->NumberOfFunctions; 1342 1342 nrNameExports = ped->NumberOfNames; 1343 1343 1344 1344 int ord, RVAExport; 1345 1345 char *name; 1346 1346 for(i=0;i<ped->NumberOfNames;i++) 1347 1347 { 1348 fForwarder = FALSE; 1349 ord = ptrOrd[i] + ped->Base; 1350 name = (char *)((ULONG)ptrNames[i] + (ULONG)win32file); 1351 RVAExport = ptrAddress[ptrOrd[i]]; 1352 1353 /* forwarder? ulRVA within export directory. */ 1354 if(RVAExport > oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress && 1355 RVAExport < oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress 1356 + oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) 1357 { 1358 fForwarder = AddForwarder(oh.ImageBase + RVAExport, name, ord); 1359 } 1360 1361 if(!fForwarder) 1362 { 1363 //points to code (virtual address relative to oh.ImageBase 1364 AddNameExport(oh.ImageBase + RVAExport, name, ord); 1365 dprintf((LOG, "address 0x%x %s @%d (0x%08x)", RVAExport, name, ord, realBaseAddress + RVAExport)); 1366 } 1367 } 1368 1348 fForwarder = FALSE; 1349 ord = ptrOrd[i] + ped->Base; 1350 name = (char *)((ULONG)ptrNames[i] + (ULONG)win32file); 1351 RVAExport = ptrAddress[ptrOrd[i]]; 1352 1353 /* forwarder? ulRVA within export directory. */ 1354 if(RVAExport > oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress && 1355 RVAExport < oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress 1356 + oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) 1357 { 1358 fForwarder = AddForwarder(oh.ImageBase + RVAExport, name, ord); 1359 } 1360 if(!fForwarder) { 1361 //points to code (virtual address relative to oh.ImageBase 1362 AddNameExport(oh.ImageBase + RVAExport, name, ord); 1363 dprintf((LOG, "address 0x%x %s @%d (0x%08x)", RVAExport, name, ord, realBaseAddress + RVAExport)); 1364 } 1365 } 1369 1366 for(i=0;i<max(ped->NumberOfNames,ped->NumberOfFunctions);i++) 1370 1367 { 1371 fForwarder = FALSE; 1372 ord = ped->Base + i; //Correct?? 1373 RVAExport = ptrAddress[i]; 1374 /* forwarder? ulRVA within export directory. */ 1375 if(RVAExport > oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress && 1376 RVAExport < oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress 1377 + oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) 1378 { 1379 fForwarder = AddForwarder(oh.ImageBase + RVAExport, NULL, ord); 1380 } 1381 1382 if(!fForwarder && RVAExport) 1383 { 1384 //points to code (virtual address relative to oh.ImageBase 1385 dprintf((LOG, "ord %d at 0x%08x (0x%08x)", ord, RVAExport, realBaseAddress + RVAExport)); 1386 AddOrdExport(oh.ImageBase + RVAExport, ord); 1387 } 1368 fForwarder = FALSE; 1369 ord = ped->Base + i; //Correct?? 1370 RVAExport = ptrAddress[i]; 1371 /* forwarder? ulRVA within export directory. */ 1372 if(RVAExport > oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress && 1373 RVAExport < oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress 1374 + oh.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size) 1375 { 1376 fForwarder = AddForwarder(oh.ImageBase + RVAExport, NULL, ord); 1377 } 1378 if(!fForwarder && RVAExport) { 1379 //points to code (virtual address relative to oh.ImageBase 1380 dprintf((LOG, "ord %d at 0x%08x (0x%08x)", ord, RVAExport, realBaseAddress + RVAExport)); 1381 AddOrdExport(oh.ImageBase + RVAExport, ord); 1382 } 1388 1383 } 1389 1384 } 1390 1391 1385 return(TRUE); 1392 1386 } … … 1395 1389 void Win32PeLdrImage::AddNameExport(ULONG virtaddr, char *apiname, ULONG ordinal, BOOL fAbsoluteAddress) 1396 1390 { 1397 ULONG nsize; 1398 int iApiNameLength = strlen(apiname); 1399 1400 if(nameexports == NULL) 1401 { 1402 nameExportSize= 4096; 1403 nameexports = (NameExport *)malloc(nameExportSize); 1404 curnameexport = nameexports; 1405 } 1406 1407 nsize = (ULONG)curnameexport - (ULONG)nameexports; 1408 if(nsize + sizeof(NameExport) + iApiNameLength > nameExportSize) 1409 { 1410 nameExportSize += 4096; 1411 char *tmp = (char *)nameexports; 1412 nameexports = (NameExport *)malloc(nameExportSize); 1413 memcpy(nameexports, tmp, nsize); 1414 curnameexport = (NameExport *)((ULONG)nameexports + nsize); 1415 free(tmp); 1416 } 1417 1418 if(fAbsoluteAddress) //forwarders use absolute address 1419 curnameexport->virtaddr = virtaddr; 1420 else 1421 curnameexport->virtaddr = realBaseAddress + (virtaddr - oh.ImageBase); 1422 1423 curnameexport->ordinal = ordinal; 1424 *(ULONG *)curnameexport->name = 0; 1425 strcpy(curnameexport->name, apiname); 1426 1427 curnameexport->nlength = iApiNameLength + 1; 1428 if(curnameexport->nlength < sizeof(curnameexport->name)) 1429 curnameexport->nlength = sizeof(curnameexport->name); 1430 1431 curnameexport = (NameExport *)((ULONG)curnameexport->name + curnameexport->nlength); 1432 } 1433 //****************************************************************************** 1434 //****************************************************************************** 1435 void Win32PeLdrImage::AddOrdExport(ULONG virtaddr, 1436 ULONG ordinal, 1437 BOOL fAbsoluteAddress) 1438 { 1439 if(ordexports == NULL) 1440 { 1441 ordexports = (OrdExport *)malloc(nrOrdExports * sizeof(OrdExport)); 1442 curordexport = ordexports; 1443 } 1444 1445 if(fAbsoluteAddress) //forwarders use absolute address 1446 curordexport->virtaddr = virtaddr; 1447 else 1448 curordexport->virtaddr = realBaseAddress + (virtaddr - oh.ImageBase); 1449 1450 curordexport->ordinal = ordinal; 1451 curordexport++; 1452 } 1453 //****************************************************************************** 1454 //****************************************************************************** 1455 BOOL Win32PeLdrImage::AddForwarder(ULONG virtaddr, 1456 char *apiname, 1457 ULONG ordinal) 1458 { 1459 char *forward = (char *)(realBaseAddress + (virtaddr - oh.ImageBase)); 1460 char *forwarddll, *forwardapi, *forwardapi0; 1461 Win32DllBase *WinDll; 1462 DWORD exportaddr; 1463 int forwardord; 1464 int iForwardApiLength; /* save strlen result */ 1465 1466 // 2001-06-01 PH 1467 // we do exactly know which character we replace: the "." is zeroed 1468 // so we can cheaply restore the name at the end of the method. 1469 // forwarddll = strdup(forward); 1470 // if(forwarddll == NULL) 1471 // { 1472 // return FALSE; 1473 // } 1474 forwarddll = forward; 1475 1476 forwardapi = strchr(forwarddll, '.'); 1477 if(forwardapi == NULL) 1478 { 1479 goto fail; 1480 } 1481 *forwardapi0 = 0; 1482 forwardapi=forwardapi0++; 1483 iForwardApiLength = strlen(forwardapi); 1484 1485 if(strlen(forwarddll) == 0 || iForwardApiLength == 0) 1486 { 1487 goto fail; 1488 } 1489 1490 WinDll = Win32DllBase::findModule(forwarddll); 1491 if(WinDll == NULL) 1492 { 1493 WinDll = loadDll(forwarddll); 1494 if(WinDll == NULL) 1495 { 1496 dprintf((LOG, "ERROR: couldn't find forwarder %s.%s", forwarddll, forwardapi)); 1497 goto fail; 1498 } 1499 } 1500 1501 //check if name or ordinal forwarder 1502 if(*forwardapi >= '0' && *forwardapi <= '9') 1503 { 1504 forwardord = atoi(forwardapi); 1505 } 1506 else 1391 ULONG nsize; 1392 1393 if(nameexports == NULL) { 1394 nameExportSize= 4096; 1395 nameexports = (NameExport *)malloc(nameExportSize); 1396 curnameexport = nameexports; 1397 } 1398 nsize = (ULONG)curnameexport - (ULONG)nameexports; 1399 if(nsize + sizeof(NameExport) + strlen(apiname) > nameExportSize) { 1400 nameExportSize += 4096; 1401 char *tmp = (char *)nameexports; 1402 nameexports = (NameExport *)malloc(nameExportSize); 1403 memcpy(nameexports, tmp, nsize); 1404 curnameexport = (NameExport *)((ULONG)nameexports + nsize); 1405 free(tmp); 1406 } 1407 if(fAbsoluteAddress) {//forwarders use absolute address 1408 curnameexport->virtaddr = virtaddr; 1409 } 1410 else curnameexport->virtaddr = realBaseAddress + (virtaddr - oh.ImageBase); 1411 curnameexport->ordinal = ordinal; 1412 *(ULONG *)curnameexport->name = 0; 1413 strcpy(curnameexport->name, apiname); 1414 1415 curnameexport->nlength = strlen(apiname) + 1; 1416 if(curnameexport->nlength < sizeof(curnameexport->name)) 1417 curnameexport->nlength = sizeof(curnameexport->name); 1418 1419 curnameexport = (NameExport *)((ULONG)curnameexport->name + curnameexport->nlength); 1420 } 1421 //****************************************************************************** 1422 //****************************************************************************** 1423 void Win32PeLdrImage::AddOrdExport(ULONG virtaddr, ULONG ordinal, BOOL fAbsoluteAddress) 1424 { 1425 if(ordexports == NULL) { 1426 ordexports = (OrdExport *)malloc(nrOrdExports * sizeof(OrdExport)); 1427 curordexport = ordexports; 1428 } 1429 if(fAbsoluteAddress) {//forwarders use absolute address 1430 curordexport->virtaddr = virtaddr; 1431 } 1432 else curordexport->virtaddr = realBaseAddress + (virtaddr - oh.ImageBase); 1433 1434 curordexport->ordinal = ordinal; 1435 curordexport++; 1436 } 1437 //****************************************************************************** 1438 //****************************************************************************** 1439 BOOL Win32PeLdrImage::AddForwarder(ULONG virtaddr, char *apiname, ULONG ordinal) 1440 { 1441 char *forward = (char *)(realBaseAddress + (virtaddr - oh.ImageBase)); 1442 char *forwarddll, *forwardapi; 1443 Win32DllBase *WinDll; 1444 DWORD exportaddr; 1445 int forwardord; 1446 1447 forwarddll = strdup(forward); 1448 if(forwarddll == NULL) { 1449 return FALSE; 1450 } 1451 forwardapi = strchr(forwarddll, '.'); 1452 if(forwardapi == NULL) { 1453 goto fail; 1454 } 1455 *forwardapi++ = 0; 1456 if(strlen(forwarddll) == 0 || strlen(forwardapi) == 0) { 1457 goto fail; 1458 } 1459 WinDll = Win32DllBase::findModule(forwarddll); 1460 if(WinDll == NULL) { 1461 WinDll = loadDll(forwarddll); 1462 if(WinDll == NULL) { 1463 dprintf((LOG, "ERROR: couldn't find forwarder %s.%s", forwarddll, forwardapi)); 1464 goto fail; 1465 } 1466 } 1467 //check if name or ordinal forwarder 1507 1468 forwardord = 0; 1508 1509 if(forwardord != 0 || (iForwardApiLength == 1 && *forwardapi == '0')) 1510 exportaddr = WinDll->getApi(forwardord); 1511 else 1512 exportaddr = WinDll->getApi(forwardapi); 1513 1514 if(apiname) 1515 { 1516 dprintf((LOG, "address 0x%x %s @%d (0x%08x) forwarder %s.%s", virtaddr - oh.ImageBase, apiname, ordinal, virtaddr, forwarddll, forwardapi)); 1517 AddNameExport(exportaddr, apiname, ordinal, TRUE); 1518 } 1519 else 1520 { 1521 dprintf((LOG, "address 0x%x @%d (0x%08x) forwarder %s.%s", virtaddr - oh.ImageBase, ordinal, virtaddr, forwarddll, forwardapi)); 1522 AddOrdExport(exportaddr, ordinal, TRUE); 1523 } 1524 1525 // free(forwarddll); 1526 *forwardapi0 = '.'; 1527 return TRUE; 1528 1529 fail: 1530 // free(forwarddll); 1531 *forwardapi0 = '.'; 1469 if(*forwardapi >= '0' && *forwardapi <= '9') { 1470 forwardord = atoi(forwardapi); 1471 } 1472 if(forwardord != 0 || (strlen(forwardapi) == 1 && *forwardapi == '0')) { 1473 exportaddr = WinDll->getApi(forwardord); 1474 } 1475 else exportaddr = WinDll->getApi(forwardapi); 1476 1477 if(apiname) { 1478 dprintf((LOG, "address 0x%x %s @%d (0x%08x) forwarder %s.%s", virtaddr - oh.ImageBase, apiname, ordinal, virtaddr, forwarddll, forwardapi)); 1479 AddNameExport(exportaddr, apiname, ordinal, TRUE); 1480 } 1481 else { 1482 dprintf((LOG, "address 0x%x @%d (0x%08x) forwarder %s.%s", virtaddr - oh.ImageBase, ordinal, virtaddr, forwarddll, forwardapi)); 1483 AddOrdExport(exportaddr, ordinal, TRUE); 1484 } 1485 free(forwarddll); 1486 return TRUE; 1487 1488 fail: 1489 free(forwarddll); 1532 1490 return FALSE; 1533 1491 } … … 1622 1580 dprintf((LOG, "********************** Finished Loading Module %s ", modname )); 1623 1581 dprintf((LOG, "**********************************************************************" )); 1624 1625 1582 1626 1583 return WinDll; … … 1859 1816 1860 1817 free(pszModules); 1861 1862 1818 return TRUE; 1863 1819 } … … 1898 1854 NameExport *curexport; 1899 1855 ULONG ulAPIOrdinal; /* api requested by ordinal */ 1900 1856 1901 1857 apilen = strlen(name) + 1; 1902 1858 if(apilen < 4) … … 1915 1871 *(ULONG *)curexport->name == *(ULONG *)apiname) 1916 1872 { 1917 if(strcmp(curexport->name, apiname) == 0) 1918 { 1919 return(curexport->virtaddr); 1920 } 1873 if(strcmp(curexport->name, apiname) == 0) 1874 return(curexport->virtaddr); 1921 1875 } 1922 1876 curexport = (NameExport *)((ULONG)curexport->name + curexport->nlength); … … 1928 1882 ULONG Win32PeLdrImage::getApi(int ordinal) 1929 1883 { 1930 ULONG apiaddr, i; 1931 OrdExport *curexport; 1932 NameExport *nexport; 1933 1934 curexport = ordexports; 1935 for(i=0;i<nrOrdExports;i++) 1936 { 1937 if(curexport->ordinal == ordinal) 1938 return(curexport->virtaddr); 1939 curexport++; 1940 } 1941 1942 //Name exports also contain an ordinal, so check this 1943 nexport = nameexports; 1944 for(i=0;i<nrNameExports;i++) 1945 { 1946 if(nexport->ordinal == ordinal) 1947 return(nexport->virtaddr); 1948 1949 nexport = (NameExport *)((ULONG)nexport->name + nexport->nlength); 1950 } 1951 return(0); 1884 ULONG apiaddr, i; 1885 OrdExport *curexport; 1886 NameExport *nexport; 1887 1888 curexport = ordexports; 1889 1890 i = 0; 1891 if(nrOrdExports > 1000) { 1892 for(i=0;i<nrOrdExports;i+=1000) { 1893 if(curexport[i].ordinal == ordinal) 1894 return(curexport[i].virtaddr); 1895 else 1896 if(ordinal < curexport[i].ordinal) { 1897 if(i) i -= 1000; 1898 break; 1899 } 1900 } 1901 if(i >= nrOrdExports) i -= 1000; 1902 } 1903 1904 if(nrOrdExports > 100) { 1905 for(i;i<nrOrdExports;i+=100) { 1906 if(curexport[i].ordinal == ordinal) 1907 return(curexport[i].virtaddr); 1908 else 1909 if(ordinal < curexport[i].ordinal) { 1910 if(i) i -= 100; 1911 break; 1912 } 1913 } 1914 if(i >= nrOrdExports) i -= 100; 1915 } 1916 1917 if(nrOrdExports > 10) { 1918 for(i;i<nrOrdExports;i+=10) { 1919 if(curexport[i].ordinal == ordinal) 1920 return(curexport[i].virtaddr); 1921 else 1922 if(ordinal < curexport[i].ordinal) { 1923 if(i) i -= 10; 1924 break; 1925 } 1926 } 1927 if(i >= nrOrdExports) i -= 10; 1928 } 1929 for(i;i<nrOrdExports;i++) { 1930 if(curexport[i].ordinal == ordinal) 1931 return(curexport[i].virtaddr); 1932 } 1933 1934 //Name exports also contain an ordinal, so check this 1935 nexport = nameexports; 1936 for(i=0;i<nrNameExports;i++) { 1937 if(nexport->ordinal == ordinal) 1938 return(nexport->virtaddr); 1939 1940 nexport = (NameExport *)((ULONG)nexport->name + nexport->nlength); 1941 } 1942 return(0); 1952 1943 } 1953 1944 //******************************************************************************
Note:
See TracChangeset
for help on using the changeset viewer.